一.什么是继承与多态?
1.继承
- 2.多态
- 多态是面向对象编程的另一个核心特性,它允许我们以统一的接口处理不同的对象。在Java中,多态通常通过方法的重写(Override)和重载(Overload)以及接口(Interface)和抽象类(Abstract Class)来实现。
-
- 继承常用于表示“是一个”的关系,比如“猫是动物”的关系。
- 多态常用于表示“能做什么”的关系,比如“这个动物能叫”的接口,可以由不同的动物类实现。
- 运行时行为:
- 继承是静态的,在编译时就已经确定。
- 多态是动态的,在运行时根据对象的实际类型确定调用的方法
总的来说,继承和多态是面向对象编程中相辅相成的两个特性,它们共同支持了代码的复用、灵活性和可扩展性。
二。抽象类与接口
- 子类可以继承父类的公有(public)和保护(protected)成员(属性和方法),但不能直接访问私有(private)成员。
- 子类可以添加新的成员或重写(override)继承的方法,以提供特定的实现。
- Java中的继承是单一继承,即一个类只能直接继承自一个父类。
- 。
在Java中,抽象类和接口都是用来定义一组方法的规范
抽象类(Abstract Class)
抽象类是一种不能被实例化的类,它通常包含一些抽象方法(即只有声明没有实现的方法)。抽象类的主要目的是为子类提供一个公共的类型,以及强迫子类实现特定的方法。
定义抽象类:
- 使用
abstract
关键字来声明一个类为抽象类。 - 抽象类中可以包含抽象方法和非抽象方法(即实现了的方法)。
- 抽象方法也使用
abstract
关键字声明,并且没有方法体(即只有方法声明,后面跟着分号)。接口(Interface)
接口是一种引用类型,是一种完全抽象的类,它仅包含常量和抽象方法的声明。接口是一种特殊的类,它不能被实例化。接口定义了一组方法规范,但不实现它们,具体的实现由实现了接口的类(称为接口的“实现类”)来完成。
定义接口:
- 使用
interface
关键字来声明一个接口。 - 接口中的所有方法默认都是抽象的(即只有方法声明,没有方法体),并且默认是
public
的。 - 接口中也可以包含常量,这些常量默认是
public static final
的。抽象类和接口的区别
-
抽象类可以有抽象方法和非抽象方法,而接口中的方法只能是抽象方法(Java 8及以后允许接口中包含默认方法和静态方法,但它们也是没有方法体的)。
-
抽象类使用
extends
关键字来继承,一个类只能直接继承一个抽象类(Java不支持多重继承抽象类,但可以通过实现多个接口来间接实现)。接口使用implements
关键字来实现,一个类可以实现多个接口。 -
抽象类可以有构造函数,而接口不能有构造函数。
-
抽象类是类层次结构的一部分,而接口是完全不同的类型。
-
接口中的所有成员(字段和方法)默认都是
public
的,而抽象类中的成员访问权限是灵活的。 -
接口是一种特殊的类型,用于定义对象的行为。它主要关注对象能做什么(即方法),而不关注对象是什么(即属性和类的具体实现)。而抽象类则更偏向于定义对象的“是什么”,并提供部分实现
三,Java内存模型
Java 内存模型(Java Memory Model,简称 JMM)是一种规范,它定义了 Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,即规范了 Java 虚拟机与计算机内存之间是如何协同工作的。具体来说,它规定了一个线程如何和何时可以看到其他线程修改过的共享变量的值,以及在必须时如何同步地访问共享变量,提供了 happens-before 原则以及 volatile 关键字、synchronized 等工具来实现内存可见性和一致性的保障。这样,程序员在编写并发代码时,可以依据这些规则来确保代码的正确执行,从而避免由于多线程带来的不确定性和错误。
Java 内存模型存在的原因在于解决多线程环境下并发执行时的内存可见性和一致性问题。在现代计算机系统中,尤其是多处理器架构下,每个处理器都有自己的高速缓存,而主内存(RAM)是所有处理器共享的数据存储区域。当多个线程同时访问和修改同一块共享数据时,如果没有适当的同步机制,就可能导致以下问题:
- CPU 和 内存一致性问题。
- 指令重排序问题。
Java 内存模型主要包括以下内容:
- 主内存(Main Memory):所有线程共享的内存区域,包含了对象的字段、方法和运行时常量池等数据。
- 工作内存(Working Memory):每个线程拥有自己的工作内存,用于存储主内存中的数据的副本,线程只能直接操作工作内存中的数据。
- 内存间交互操作:线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将修改后的数据刷新到主内存。
- 原子性(Atomicity):JMM 保证基本数据类型(如 int、long)的读写操作具有原子性,即不会被其他线程干扰,保证操作的完整性。
- 可见性(Visibility):JMM 确保一个线程对共享变量的修改对其他线程可见。这意味着一个线程在工作内存中修改了数据后,必须将最新的数据刷新到主内存,以便其他线程可以读取到更新后的数据。
- 有序性(Ordering):JMM 保证程序的执行顺序按照一定的规则进行,不会出现随机的重排序现象。这包括了编译器重排序、处理器重排序和内存重排序等
四、GC垃圾回收
通过GC,Java自动管理对象的生命周期,回收不再使用的对象所占的内存空间
标记-清除算法的主要缺点是清除后会产生内存碎片。
复制(Copying)算法
- 标记阶段:标记所有存活的对象。
- 清除阶段:回收所有未标记的对象。
- 压缩阶段(可选):整理内存碎片。
- 标记-清除(Mark-Sweep)算法
-
标记-清除算法是最基本的垃圾回收算法,分为两个阶段:
- 标记阶段:从根集合(GC Roots)开始,递归标记所有可达的对象。
- 清除阶段:遍历整个堆,回收未标记的对象。
复制算法将内存分为两个区域,每次只使用其中一个区域。当活动区域用完时,将存活的对象复制到另一块区域,然后清空当前区域。
a. 复制阶段:将所有存活的对象从使用的区域复制到空闲区域。
b. 交换区域:清空当前区域,并交换使用和空闲区域的角色。
复制算法的主要优点是没有内存碎片,缺点是需要双倍的内存空间。
————————————————
标记-压缩(Mark-Compact)算法
标记-压缩算法结合了标记-清除和复制算法的优点。它在标记阶段标记所有存活对象,然后在压缩阶段将存活对象移动到堆的一端,释放出连续的内存空间。
- a. 标记阶段:标记所有存活对象。
- b. 压缩阶段:将存活对象移动到堆的一端,按顺序排列
2.4 分代收集(Generational Collection)算法
分代收集算法基于对象的存活时间,将堆内存分为几代:年轻代(Young Generation)、年老代(Old Generation)和永久代(Permanent Generation)。各代使用不同的收集算法。
年轻代:对象生命周期短,频繁发生GC,使用复制算法。
年老代:对象生命周期长,使用标记-清除或标记-压缩算法。
永久代:存储类的元数据(在Java 8及以后版本中被元空间(Metaspace)替代)。
————————————————
五,java反射机制
反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。 这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制
哪里用到了反射机制: JDBC中加载数据库驱动,框架编写
反射机制的优缺点:在程序执行时可以动态执行方法访问属性,但是反射机制这类操作总是执行速度比直接执行java代码还慢
反射机制的作用:在程序运行时动态判断一个对象所属的类,一个类里面的属性,方法。
反射的实现方式:
1)Class.forName(“类的路径”);
2)类名.class
3)对象名.getClass()
4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
六,"equals"和"=="比较的区别
- "=="比较基本数据类型时比较的是表面值内容,而比较两个对象时比较的是两个对象的内存地址值。
- equals方法不能作用于基本数据类型的变量。
- 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址。
- 诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
- ==在基本数据类型时比较的是值内容,引用类型时比较的是地址。
七,重载和重写的区别,他们的编译方式
重载是在一个类中可以定义多个相同方法名称的方法,但他们的参数列表不同,重写是子类对父类的方法进行重写,重写的方法返回值,方法列表都一样。
区别
- 参数不同
重载是通过方法的参数列表不同来区分同名方法的,而重写是通过子类重新定义父类方法来实现的。
- 解析时机(编译时与运行时)
重载是在编译时确定调用哪个方法,而重写是在运行时动态绑定,根据对象的实际类型调用相应的方法。
- 返回值
在重载中,方法的签名包括方法的名称、参数数量、类型和顺序,重载方法的返回值类型不是方法签名的一部分,而重写方法的返回值类型必须与被重写方法的返回值类型相同。
八,数组和链表的区别
数组和链表都是基本的数据结构,用来存储一系列元素。
内存分配:数组需要一块连续的存储空间,可能会造成内存浪费,而链表是离散存放的,之间通过指针进行连接,内存利用效率提高。
访问方式:数组可以通过下标任意快速访问指定的元素,链表必须从头结点开始遍历查找。
大小可变:数组大小一旦定义就不可改变,而链表根据需求可以动态的添加,删除某些结点。
插入删除:数组在插入删除节点时,可能需要移动其他节点保证顺序,时间复杂度为O(n),链表只需要更改指针指向即可,时间复杂度为O(1)
使用场景:数组适用于大量快速随机访问元素,链表适用于大量需要插入删除元素。
九,String和StringBuffer、StringBuider的区别
十,类加载机制和对象实例化的过程
类加载机制可分为阶段:加载,连接,初始化。
如果没有意外,JVM会连续完成这三个步骤,所以一般把这三个步骤统称为类的加载或类初始化。另外,如果该类的直接父类还没有被加载,则先加载该类的父类。
加载、验证、准备、初始化和卸载这5个阶段的开始顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
加载指的是把class二进制文件读入内存,并创建一个class对象,通常由类加载器完成
连接是把类的二进制数据合并到JRE中,又分为验证(确保数据符合JRE规范),准备(为类的静态字段赋初值),解析(将符号引用替换为直接引用)
初始化:对静态字段进行初始化,执行初始化语句。
对象实例化的过程:
分配内存的主要方式有两种:指针碰撞和空闲列表。选择采用指针碰撞还是空闲列表法分配内存,主要由Java堆内存是否规整决定的,而Java堆内存是否规整又取决于所采用的垃圾收集算法
在为对象分配内存完成之后,虚拟机会将分配到的这块内存初始化为零值,这样也就使得Java中的对象的实例变量可以在不赋初值的情况下使用,因为代码所访问当的就是虚拟机为这块内存分配的零值。
对象头就像我们人的身份证一样,存放了一些标识对象的数据
虚拟机将调用实例构造器方法(), 根据我们程序员的意愿初始化对象,在这一步会调用构造函数,完成实例对象的初始化。
堆内存中已经存在被完成创建完成的对象,但是我们知道,在Java中使用对象是通过虚拟机栈中的引用来获取对象属性,调用对象的方法,因此这一步将创建对象的引用,并压如虚拟机栈中,最终返回引用供我们使用。
十一,JDK1.7和1.8的区别
JDK1.7
1.1二进制变量的表示,支持将整数类型用二进制来表示,用0b开头。
1.2 Switch语句支持String类型。
1.3 Try-with-resource语句:
ry-with-resources语句是一种声明了一种或多种资源的try语句。资源是指在程序用完了之后必须要关闭的对象。
try-with-resources语句保证了每个声明了的资源在语句结束的时候都会被关闭。任何实现了java.lang.AutoCloseable接口的对象,和实现了java.io.Closeable接口的对象,都可以当做资源使用。
1.4 Catch多个异常
1.5 数字类型的下划线表示 更友好的表示方式,不过要注意下划线添加的一些标准
1.6 泛型实例的创建可以通过类型推断来简化 可以去掉后面new部分的泛型类型,只用<>就可以了。
1.7在可变参数方法中传递非具体化参数,改进编译警告和错误
1.8 信息更丰富的回溯追踪 就是上面try中try语句和里面的语句同时抛出异常时,异常栈的信息等等
JDK1.8
java 1.8 是1.7的增强版,新增了以下特性
1、default关键字,在接口中可以通过使用default关键字编写方法体,实现类可以不用实现该方法,可以进行直接调用
2、Lambda 表达式,函数式编程,函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量
3、函数式接口,“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。jdk1.8提供了一个@FunctionalInterface注解来定义函数式接口,如果我们定义的接口不符合函数式的规范便会报错。
4、方法与构造函数引用,jdk1.8提供了另外一种调用方式::,当 你 需 要使用 方 法 引用时 , 目 标引用 放 在 分隔符::前 ,方法 的 名 称放在 后 面
5、Date Api更新1.8之前JDK自带的日期处理类非常不方便,我们处理的时候经常是使用的第三方工具包,比如commons-lang包等。不过1.8出现之后这个改观了很多,比如日期时间的创建、比较、调整、格式化、时间间隔等。这些类都在java.time包下。比原来实用了很多。
6、流,流是Java API的新成员,它允许我们以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,我们可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,也就是说我们不用写多线程代码了。
堆内存溢出、方法区内存溢出和栈内存溢出
堆内存溢出是最常见的OOM场景之一。它通常发生在以下情况:
对象过多:应用程序创建了大量的对象,并且这些对象长时间存活,导致堆内存不足。 内存泄漏:应用程序中存在内存泄漏,即长时间无法释放不再使用的对象,导致堆内存持续占用。
实战解决方案
- 优化代码和数据结构:减少不必要的对象创建,使用合适的数据结构来存储数据,避免过大的集合和数组。
- 内存泄漏检测:利用内存分析工具(如MAT、VisualVM)进行堆内存转储和分析,找出内存泄漏的根源,并及时修复。
- 调整JVM参数:根据服务器的物理内存大小,适当调整JVM的堆内存大小。通过
-Xmx
和-Xms
参数设置堆内存的最大值和初始值,避免频繁的内存扩展和收缩。
方法区内存溢出通常与类的加载和元数据的存储有关。主要原因包括:
- 类加载过多:应用程序加载了大量的类,并且这些类的元数据占用了过多的方法区内存。
- 类加载器泄露:自定义的类加载器未正确实现或第三方库导致的类加载器泄露,无法释放已加载的类。
栈内存溢出通常与线程的执行和递归调用有关。主要原因包括:
递归调用过深:递归算法实现不当,导致递归深度过大,超出了线程栈的大小限制。 线程创建过多:应用程序创建了大量的线程,并且每个线程的栈内存分配过多,导致系统资源耗尽。
实战解决方案
OOM是一个常见的Java应用程序问题,但通过深入理解和分析JVM的内存管理机制,我们可以采取相应的实战解决方案来避免或解决这个问题。在堆内存溢出方面,要优化代码和数据结构、检测内存泄漏、调整JVM参数;在方法区内存溢出方面,要限制方法区大小、检查类加载器实现、优化类加载策略;在栈内存溢出方面,要优化递归算法、调整线程栈大小、限制线程数量。通过合理的优化和配置,我们可以提升Java应用程序的稳定性和性能
十三,static和代码块之间的执行顺序
static基本使用场景
修饰类
修饰属性
修饰方法
静态导包
静态代码块
- 优化递归算法:重新设计递归算法,减少递归深度,或者考虑使用非递归的实现方式来替代递归调用。
- 调整线程栈大小:通过
-Xss
参数设置线程栈的大小。但是要注意不要设置过大,以免消耗过多的系统资源。需要根据应用程序的实际情况进行调整。 - 限制线程数量:使用线程池来管理线程的创建和销毁,避免创建过多的线程。同时,注意合理配置线程池的参数,以满足应用程序的需求。
- 分析和定位问题:使用线程分析工具(如jstack)获取线程栈信息,找出导致栈溢出的具体线程和调用栈。根据分析结果调整代码逻辑,避免过深的递归调用或不必要的线程创建。
基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块
Java静态代码块中的代码会在类加载JVM时运行,且只被执行一次
2、静态块常用来执行类属性的初始化
3、静态块优先于各种代码块以及构造函数,如果一个类中有多个静态代码块,会按照书写顺序依次执行
4、静态代码块可以定义在类的任何地方中除了方法体中【这里的方法体是任何方法体】
5、静态代码块不能访问普通变量
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_44543508/article/details/102593419
代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块
继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器