Java基础知识整理
String、StringBuilder、StringBuffer相关
-
String、StringBuilder、StringBuffer有什么区别与联系?
- 三兄弟的底层实现,jdk9之前是一个final修饰的char[]数组来保存字符,字符串的每个字符占2个字节。而jdk9之后采用final修饰的byte[]数组外加一个encoding-flag字段来保存字符,因此每个字符只占一个字节。从而使得字符串更加节省空间
- String为不可变的字符序列,每次对String进行操作都是生成新的String对象,这样效率低下且十分占内存空间。其是线程安全的
- StringBuilder和StringBuffer都继承于AbstractStringBuilder,它两都是可变的字符序列,其中StringBuilder线程不安全,执行速度较快。而StringBuffer线程安全,里面的方法都添加了synchronized关键字,执行速度相对于StringBuilder慢。
- 相比String,StringBuilder和StringBuffer类的对象能够被多次修改且不会生成新的未使用对象。其中在对字符串进行大量操作的时候,根据是否多线程去选择StringBuffer还是StringBuilder。
-
String是final类型的,不可变,但是通过反射可以暴力修改。
- 为了实现字符串池:String对象是缓存在字符串池中的,因此这些缓存的字符串是可以被多个客户端访问的,如果一个客户端的访问影响了别的客户端的行为,这样就存在风险。intern方法的调用:一个初始化为空的字符串池,它由String独自维护,当调用intern方法时,如果池已经包含了一个等于此String对象的字符串(equals)则返回池中的字符串,否则将此String对象添加到池中,并返回此String对象的引用。
- 防止通过继承String类,覆盖父类的方法。一旦继承将会破坏String的不可变性、缓存性以及hascode的计算方式。
- 为了线程安全:同一个字符串实例可以被多个线程共享,这样便不用因为线程安全问题而使用同步。
- 为了实现String可以创建hashCode不可变性:其创建的时候hashCode就被缓存了,不需要重新计算,这样使得字符串很适合作为HashMap中的键,字符串的处理速度要快于其他的对象。
- 确保数据安全性:例如用户名密码等。
-
Java语言规范对String做了如下说明:
- 每一个字符串常量都指向字符串池中或者堆内存中的一个字符串实例。
- 字符串对象值是固定的,一旦创建就不能再修改。
- 字符串常量或者常量表达式中的字符串都被使用方法String.intern()在字符串池中保留了唯一的实例。
-
String a = new String(“a”); 创建了两个对象或者一个
- 两个对象:在Java堆中保存一个a对象,且在常量池中也有一个"a"。
- 一个对象:常量池中已有"a",jvm将这个"a"直接赋给当前引用,不会在常量池中创建新的。
-
Java中的常量优化会使当前已知的字面量"a" + “b” + “c” 优化为"abc",而不是在常量池创建三个对象。
-
而类似这种下面的情况,则不会这么做,此时的s3指向的是堆中的字符串对象。
-
==与equals与hashCode
-
==:判断两个变量或实例是否指向同一个内存空间,即判断两个对象(的引用)是否是同一个。基本数据比较的是值,引用数据比较的是内存地址
-
equals:判断两个对象的内容是否相等,equals是判断两个变量或实例所指向的内存空间的值是否相同。或者字符串的内容是否相同
-
String中重写了equals来让其去比较对象的值,倒不如说一般都建议重写equals,因为底层源码中,equals就是==
-
public boolean equals(Object obj) {return (this == obj); }
-
-
hashCode与equals和==的一些相关规定
- 两个对象相等,hashCode一定相同,==和equals返回一定为true。
- 两个对象的hashCode相等,他们不一定相等。
- 两个对象的hashCode不相等,他们一定不是同一个对象。
-
hashCode通用规定
-
一个程序应用在执行期间,只要对象的equals方法的比较所用到的信息没有被修改,那么对同一对象的多次调用hashCode应该返回同一个值。
-
两个对象的equals返回true,则要求调用两个对象的hashCode必须返回同一个结果。
-
两个对象的equals返回false,则不要求两个对象一定不等(重写equals的情况)。
-
这也是为什么重写equals必须重写hashCode,如果不重写的话,会有一种情况(在一些场景下我们要求equals返回true,他们的实际值相等,可他们并不是同一个对象。hashCode默认是根据对象的内存地址经过hash算法得来的,每个对象在堆中的存储不一样,那么hashCode一定不相等,违反了前面的第二条)。
-
序列化与反序列化
- 序列化:把一个对象以字节流的形式保存到磁盘中,还有一个用途是在网络上传送对象的字节序列。序列化还可以用来实现深拷贝
- 反序列化:把一个字节流恢复为原来的对象。其读取的是Java对象中的数据,而不是Java类,所以反序列化要求必须提供该Java对象所属类的class文件。
- 要想实现序列化,则必须实现Serializable接口或者Externalizable接口,如果希望某个属性不被序列化,可以加上transient关键字
- 序列化的注意事项:
- 如果一个子类可序列化,则它的父类(直接或间接)要么含有无参构造器,要么也是可序列化的。
- 如果一个类的成员变量的类型除了基本类型和String类型,它还含有引用类型,则该引用类型必须是可序列化的,否则这个类就不是可序列化的。
- 在序列化的时候,对象的类名、实例变量(基本类型、String、数组、引用)都会被序列化;而方法、类变量(static)、transient修饰实例变量(也被称为瞬态实例变量)不会被序列化,在transient反序列化后,会出现0值和null值。
- 序列化ID的作用:Java序列化机制通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化的时候,JVM会把传来的字节流中的serialVersionID与本地相应实体类的serialVersionID进行比较,如果相同就认为是一致的,可以进行反序列化,否则抛出序列化版本不一致的异常。
深拷贝和浅拷贝
- 对象拷贝是将一个对象的属性拷贝到另一个有着相同类型的对象中去。调用clone方法可以实现拷贝对象,克隆对象要求必须实现Clonnable接口。拷贝对象是Java中一种创建新对象的方式。
- 浅拷贝:按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是这个基本类型的值;如果属性是引用类型,拷贝其引用但不拷贝引用的对象。因此如果其中一个对象的内存地址改变,另一个也会受到影响。从Object中继承而来的clone默认是浅拷贝。
- 深拷贝:拷贝所有属性,并给所有引用属性开辟新的内存空间,而不仅仅是拷贝引用。当对象和它所引用的对象一起拷贝时就发生深拷贝。深拷贝相比浅拷贝速度慢且花销大。实现深拷贝必须实现Clonnable接口和重写clone方法,且将该对象中引用的其他对象都要clone一份,这又要求引用其他的对象实现clonnable并重写clone方法。这么看来如果不停的引用,则这个将无穷无尽。所以实现彻底深拷贝是不可能的。
创建对象的方式
- 调用关键字new,最常见的方式,调用了构造函数。
- 使用反射机制,利用Class类的newInstance方法或者Constructor的newInstance方法,调用了构造函数。框架的设计大量使用了反射机制。
- 调用Object的clone方法,没有调用构造函数。
- 反序列化,我们进行反序列化的时候,JVM会给我们创建一个单独的对象,没有调用构造函数。
获取Class对象的方式
-
使用Class.forName(String className)方法,传入某个类的全限定类名,例如JDBC。只能在运行期确认该类是否存在。
-
调用某个类的.class属性来返回对应的class对象。代码安全,程序在编译阶段就可以检查需要访问的Class是否存在,程序性能好。
Class<ClassName> clazz = ClassName.class;
-
调用getClass()方法。
Java反射机制
-
反射:对于任意一个类,都能够知道这个类的所有属性和方法(包括静态属性和静态方法);对于任意一个对象,都能够调用它的任意一个方法和属性。即动态获取对象信息和调用对象方法的功能。反射还可以新建类的实例(对象)。
-
反射的优点:
- 极大的提高了程序的灵活性和扩展性,降低模块的耦合性
- 让程序创建和控制任何类的对象,无须提前硬编码目标类
- 可以在运行期构造一个类的对象,判断一个类所具有的的成员变量和方法,调用一个类的方法
- 框架技术的基础
-
缺点:
- 性能问题:反射机制包含了一些动态类型,故Java虚拟机不能够对这些动态代码进行优化
- 安全问题:反射技术要求程序必须在一个没有安全限制的环境中运行
- 程序健壮性问题:反射允许代码执行一些不被允许的操作,这样破坏了Java的程序结构的抽象性
-
反射的具体应用场景:
- JDBC中的Class.forName(“com.mysql.cj.jdbc.Driver”);
- Spring用反射来创建对象,根据XML配置文件信息来装载Bean,配置文件中读取的只是某个类的字符串类名,程序需要根据该字符串来创建对象
- 将程序内所有的XML或者properties配置文件加载入内存
- 在类里面解析XML或properties的内容,得到对应实体类的字节码字符串以及相关的属性信息
- 使用反射机制根据字符串获取某个类的Class实例
- 动态配置实例的属性
-
使用就是获取某个类的class对象,然后利用该对象下的各种getXxx方法即可获取。
Java的8种基本类型详解
byte | char | short | int | long | float | double | boolean | |
---|---|---|---|---|---|---|---|---|
表示范围 | -2^7 到 2^7-1 | / | -2^15 到 2^15-1 | -2^31 到 2^31-1 | -2^63 到 2^63-1 | / | / | / |
所占字节 | 1 | 2 | 2 | 4 | 8 | 4 | 8 | 1 |
其中对于int,32 64位的区别就是在对象头上: 32位系统上占用8bytes,64位系统上占用16bytes;
面试题:float是怎么存储的?
- 浮点数在计算机中占四个字节,一个浮点数由2部分组成,底数m和指数e,表示为±m × 2e,其中m和e都是二进制。其存储遵循IEEE - 754标准
- 底数部分占用一个24bit的值,其最高位始终为1,所以省去不存储,实际存储底数只有23bit。
- 指数部分占用8bit,可表示0 - 255,由于指数可正可负,所以此处算出的次方减去127才是真正的指数。
- 例如18.375的二进制为10010.011,将小数点左移四位得到1.0010011x 2^4,4+127=131,则存储的方式是:符号位0 一位,指数位10000011八位,尾数位0010011二十三位,不够补0。
面试题:char能否存储汉字?
- 因为java中的char是两个字节的,所有可以用来存储中文(一个中文也是两个字节),而在c语言中char只是一个字节,所以不能用来存储中文,要想存储中文,只能用字节数组。
- char是按照字符存储的,不管英文还是中文,固定占用占用2个字节,用来储存Unicode字符。范围在0-65536。unicode编码字符集中包含了汉字,所以,char型变量中当然可以存储汉字啦。不过,如果某个特殊的汉字没有被包含在unicode编码字符集中,那么,这个char型变量中就不能存储这个特殊汉字。
包装类相关
-
装箱:将基本类型用它们对应的引用类型来包装,例如int->Integer
-
拆箱:将包装类型转换为基本数据类型
-
自动装箱:进行创建的时候 Integer a = 127; 就将127这个基本类型自动装箱成Integer。
-
自动拆箱:在Integer类型与int进行比较时,会把Integer自动拆箱成int类型进行比较。
Integer a = 128; //将127这个基本类型自动装箱成Integer Integer b = 128; //将127这个基本类型自动装箱成Integer int c = 128; System.out.println(a == b); //不在在-128到127这个范围,会创建新的对象,所以a != b System.out.println(a.equals(b)); //比较对象的值,直接就true System.out.println(a == c); //将a自动拆箱成int的128和c进行比较,返回true
意义:Java是一种完全面向对象的语言,但是对于CPU来说,处理一个完整的对象需要很多指令,并且又需要很多内存。于是Java有一种机制,使得基本类型在一般的编程中被当做非对象的简单类型处理,另一些场合又允许他们是个对象,例如方法传值需要传递一个Object类型,还有就是泛型指定的时候。
面向对象相关
- 面向对象:无关底层,降低了程序之间的耦合性,可维护性好。抽象出一个类,里面有数据也有解决问题的方法,需要什么功能就直接用,不必一步一步实现。
- 面向过程:具体化,流程化,解决一个问题需要一步一步分析,一步一步实现。性能要优于面向对象。
- 面向对象三大特性:
- 封装:隐藏对象的属性和实现细节,仅仅对外提供公共访问的方式,提高复用性和安全性。
- 继承:使用已存在的类作为基础,建立新的类。是多态的前提,Java中没有多继承。
- 子类拥有父类非private方法
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展
- 子类可以用自己的方式实现父类的方法
- 多态:程序中定义的引用变量所指向的具体类型和通过该引用变量调用的方法在编程时并不确定,而是在程序运行的时候才确定的。多态可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变。通俗的说就是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,提高了程序的扩展性。例如List list = new ArrayList<>();
- 多态的三个必要条件:继承、重写、向上转型(父类可以访问子类从父类中继承来的方法,以及子类对象重写父类对象的方法。也就是通过子类对象实例化父类对象,是一种自动转换)
- ps:向下转型是通过父类对象(大范围)实例化子类对象(小范围)。例如开发中的User user = (User) request.getSession().getAttribute(“user”); 其中,后面的返回的是一个Object类型的。
- 面向对象五大原则:
- 单一职责原则:类功能单一,不能包罗万象,一个类一个功能。
- 开放封闭原则:一个模块对于拓展是开放的,对于修改应该是封闭的。
- 里式替换原则:子类可以替换父类出现在父类能够出现的地方。
- 依赖倒置原则:高层次模块不应该依赖于低层次模块,他们都依赖于抽象。而抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
- 接口分离原则:类似开放封闭,设计时应该采用多个与特定客户类有关的接口,而不是大杂烩通用接口。即一个接口对应一个功能。
抽象类与接口
-
相同点:
- 都不能被实例化
- 都位于继承的顶端,用于被其他的类实现或继承
- 都包含其方法,子类必须重写这些方法
- 都不能被final修饰
-
不同点:
抽象类 接口 声明 abstract interface 实现 extends,如果子类不是抽象类的话,子类需要提供抽象类所有声明的抽象方法的实现 implements,实现类需要提供所有接口中方法的实现 构造器 抽象类可以有构造器,但不能创建实例 不可以有构造器 访问修饰符 方法可以是任意的访问修饰符,字段也是 方法为隐式的public abstract;变量为public static final 多继承 一个类只能继承一个抽象类 一个类可以实现多个接口 -
Java8以后,接口中可以有default或者static的方法,当接口中定义了default的方法,就可以在接口中写方法体,子类继承的时候就可以直接拥有该方法。
- 如果一个类实现了两个接口,且该两个接口有同名的default方法,则我们必须在实现类中重写该方法;
- 如果子类继承父类,父类有一方法a,且子类实现的接口也有一同名方法a,则子类得到的是父类的方法(继承大于实现)。
-
抽象类可以有抽象方法,其他类不能有抽象方法。
成员变量与局部变量
成员变量 | 局部变量 | |
---|---|---|
作用域 | 整个类 | 某个范围内 |
存储与生命周期 | 随着对象的创建而存在,对象的消失而消失,位于堆内存中 | 方法被调用或者语句被执行的时候存在,位于占内存栈帧中 |
初始值 | 有默认初始值,如果被final修饰必须赋值 | 方法体内的使用前必须赋值 |
修饰符 | private、protected、public、static、final | 不能被static修饰 |
内部类
-
静态内部类:定义在类内部的静态类,它可以访问外部类的所有静态变量,而不能访问外部类的非静态变量
class Outer {private static int index = 1;static class StaticInner {void visit() {System.out.println("index = " + index);}} } 调用visit方法:Outer.StaticInner inner = new Outer.StaticInner();inner.visit();
-
成员内部类:定义在类内部,成员位置上的非静态类
class Outer {private int index = 1;class Inner {void visit() {System.out.println("index = " + index);}} } 调用visit方法://先创建一个outer对象,因为非静态不能直接调用Outer outer = new Outer();//然后创建内部类对象Outer.Inner inner = outer.new Inner();inner.visit();
-
局部内部类:定义在方法中的内部类
class Outer {private int index = 1;public void functionInner() {class Inner {void visit() {System.out.println("index = " + index);}}} } 调用visit方法:比较简单:Outer outer = new Outer();outer.functionInner();
-
匿名内部类:没有名字的内部类,开发中用的比较多,一般在方法后面
class Outer implements Inner {private void test(final int i) {new Inner() {@Overridepublic void visit() {System.out.println("匿名内部类" + i);}}.visit();} } interface Inner {void visit(); } //lambda表达式 class Outer implements Inner {private void test(final int i) {((Inner) () -> System.out.println("匿名内部类" + i)).visit();} } interface Inner {void visit(); }
-
匿名内部类的外部类必须继承一个抽象类或实现一个接口,可以重写父类中的方法以及自定义自己的方法。不能定义任何的静态成员和静态方法。
-
在多线程编程中,通过实现Runnable接口和Callable接口中可以使用匿名内部类。
-
当所在的方法形参或者局部变量(JDK1.8前)要被匿名内部类使用时,必须声明final。
-
方法形参:如果方法执行完,局部变量会被销毁,这样如果匿名内部类仍要使用该局部变量,就会报错。
-
局部变量:匿名内部类可以访问局部变量,是因为底层将这个局部变量的值传入到了匿名内部类中,并且以匿名内部类的成员变量的形式存在。用final修饰是为了保护数据的一致性,匿名内部类中存入的是一个引用地址,而并不是实际的变量。如果局部变量的引用发生变化,那么匿名内部类是不知道的,其里面还是指向原来的变量,如果程序运行下去可能就会有一些问题。JDK1.8后,Java底层帮我们加上了一个隐式的final。
-
-
四种访问修饰符的对比
修饰对象 | 可见范围 | |
---|---|---|
private | 变量、方法、内部类 | 同一类中 |
default | 类、接口、变量、方法 | 同一包中 |
protected | 变量、方法、内部类 | 同一包内的类和所有子类可见 |
public | 类、接口、变量、方法 | 所有类 |
- protected的坑:
- 父类的protected成员是包内可见的,并且对子类也可见
- 若子类和父类不再同一个包中,那么在子类中,子类实例可以访问其从父类继承而来的protected方法,而不能访问父类实例的protected方法。即创建父类实例的话,它就不能访问自己实例的protected方法
Object中的方法
- protected Object clone():创建并返回该对象的副本,是浅拷贝
- boolean equals(Object obj):判断一个对象是否相等于另一个对象
- protected void finalize():当垃圾收集确定不再有对该对象的引用时,垃圾收集器在对象上调用该方法。子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
- 不建议使用该方法,因为Java并不保证finalize方法会被及时的执行、而且根本不会保证他们会被执行。并且JVM通常在单独的低优先级线程中完成finalize的执行。
- Class getClass():返回此Object运行时的类
- int hashCode():返回对象的hashCode
- void notify():唤醒正在等待的单个线程
- void notifyAll():唤醒正在等待的所有线程
- String toString():返回对象的字符串表示形式
- void wait():使当前线程阻塞,并释放其所持有的锁,直到其他线程调用此对象的notify或者notifyAll()方法,使当前线程进入就绪状态。还有两个带参数的wait(参数是时间),超过时间后也使当前线程进入就绪状态
this与super关键字详解
this
- this是自身的一个对象,代表对象本身,也就是指向当前对象的指针。程序中产生歧义的时候经常用this来指明当前对象。
- 用法:
- this.属性名,就表示访问类中的成员变量,用来区分成员变量和局部变量。例如setter中的 this.xx = xx 就是把setter参数中的属性值赋给该对象。
- this.方法名:用来访问本类的成员方法。
- 直接this():访问本类的无参构造方法,()中可以有参数,如果有参数就是调用指定的有参构造,即this(name) 就调用参数为name的有参构造函数。this()不能用于普通方法,只能用于构造方法中调用本类中另一种形式的构造函数,且要放必须是构造方法中的第一条语句。
class Person {private String name;private int age;Person(String name, int age) {this(name); //调用下面的Person(String name)this.name = name;this.age = age;//this(); //直接爆红并提示:this()必须放在构造方法的第一条}private Person(String name) {this();this.name = name;}//如果不写空构造方法,就不能写this(); 方法,直接爆红,因为this本来就是调构造方法的,没有无参构造构造个锤子。private Person() {}
}
super
- 指向自己父类对象的一个指针,这个父类指的是离自己最近的父类
- 用法:
- super.xxx直接引用父类的成员
- 子类成员变量或方法与父类中的同名时,使用super进行区分
- 引用父类的构造函数,同样的是也只能位于构造方法中,且必须是第一条语句
this和super
- super是指向当前对象父类的指针;this是指向当前对象的指针
- super一般出现在子类;this出现在本类
- this和super都指向对象,所以this和super都不能在static环境下使用,包括static变量、方法、语句块。除非是在静态构造类中使用this或者super调用静态成员
static Person(String name, int age) {this(name);this.name = name; //爆红this.age = age; //爆红
}
private static Person(String name) {this();this.name = name; //爆红
}
爆红的原因是这些参数变量不属于任何对象,而是被类的实例所共享(非static不能访问static)
static关键字详解
- 创建独立于具体对象的域变量或者方法,以至于不创建对象照样能使用属性和方法,可以直接ClassName.methodName()来调用类里面的方法。
- 特性:
- 被static修饰的变量或者方法独立于该类的任何对象,即这些变量不属于任何一个实例对象,而是被类的实例对象所共享。
- **在该类第一次加载的时候,就会加载static修饰的部分,而且只在类第一次使用时加载并初始化。在内存中只有一份,类加载的过程中JVM只为其分配一次内存空间。**用来形成静态代码块以优化程序的性能。在类初次被加载的时候,会按照static块的顺序来执行每个static代码块,并且只会执行一次。所以把初始化操作一般放在static块中。而实例变量则每次创建对象都要给他分配内存,创建几次就有几份成员变量。
- 被static修饰的变量或者方法是优先于对象存在的,也就是说一个类加载完毕,即便没有创建对象也可以来访问。
- 修饰范围:成员变量、成员方法、静态代码块、修饰静态内部类。
- static代码块只能访问static、非static可以访问static和非static。
- **static方法必须被实现,不能是abstract的。static方法只能被static方法重写。**static方法可以不通过对象进行调用,因此不能存在非静态变量,也不能访问非静态变量成员。因为这些成员需要创建对象去进行初始化。
final关键字详解
-
final用于修饰类、属性、方法以及本地变量
- 修饰的类不可以被继承,但类里面的内容可以改变。
- 修饰的方法不可以被重写,但修饰的方法可以被重载。
- 修饰的成员变量必须赋予初始值,或者在该类的构造方法中赋值。该值在编译期就已经确定了,相当于编译期常量。
- 修饰的变量不可以被改变(引用不可改变,即不可以指向其他的对象,但指向内容是可变的)
- final不能用来修饰接口,final不能与abstract同时出现。
Java类加载机制
类加载的三个步骤
-
加载:将类的class文件读入到内存,并为之创建一个java.lang.Class对象
- 加载的过程:
- 通过一个类的全限定类名来获取定义的二进制字节流。
- 将这个字节流所代表的的静态存储结构转化为方法区运行时的数据结构(1.7后就是转换为元空间和堆)。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 数组本身不是由类加载器加载的,而是由JVM创建,但数组类的组件类型(去掉所有维度的类型)最终要靠类加载器去创建:
- 如果数组的组件类型是引用类型,则递归采用加载过程去加载该组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识。
- 如果不是引用类型,例如int[],jvm会把数组标记为与引导类加载器关联。
- 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。
- 类加载由类加载器完成,一般由JVM提供,当然开发者也可以通过继承ClassLoader来创建自己的类加载器。不同的类加载器可以从不同来源去加载类的二进制数据:
- 本地文件系统加载class文件,就是我们一般的写代码
- 从jar包中加载class文件
- 通过网络加载class文件
- 把一个Java源文件动态编译并执行加载
- 类加载器无须等到首次使用才加载该类,JVM允许系统预先加载某些类。
- 加载的过程:
-
链接:把类二进制数据合并到JRE中
-
**验证:用于检验被加载的类是否有正确的内部结构,以确保Class文件的字节流中包含信息是否符合当前虚拟机要求,不会危害虚拟机自身的安全。**其中包括:
- 文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。
- 元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
- 字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。
- 符号引用验证:确定访问类型等涉及到引用的情况,保证引用一定会被访问到,不会出现类等无法访问的问题。
-
准备:负责为类的静态变量分配内存,并设置默认值。
- 这些静态变量的内存分配是在方法区(1.7以前),1.7后在元空间
- 此时进行内存分配仅仅包括类变量,而不包括实例变量。实例变量是在对象实例化的时候随着对象一起分配在Java堆中。
-
解析:将类的二进制数据中的符号引用替换成直接引用。
- 符号引用:以一组符号去描述所引用的目标,符号可以是任何形式的字面量,只要不影响到定位即可,布局与内存无关。
- 直接引用:直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。与虚拟机实现的内存布局相关,引用的目标必定在内存中存在。
- 解析有一个前提:方法在程序正在运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的(调用目标在程序代码写好、编译器进行编译时就必须确定下来)。符合编译期可知,运行期不可变的方法有:静态方法、私有方法、实例构造器、父类方法、final修饰的方法。解析主要针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
-
-
初始化:为类的静态变量赋予正确的初始值
-
初始化的时机(与类加载的时机差不多,对一个类进行主动引用才初始化)
-
以下情况不会导致初始化:
- 通过子类引用父类的静态字段,不会导致子类初始化。
- 通过数组定义来引用类,不会导致该引用类初始化。
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
-
该阶段与准备阶段的区别:
-
我们假设有一语句
private static int a = 10;
其执行过程是:字节码加载到内存后,进行完链接的验证,通过后进入准备阶段,该阶段中给static分配内存并赋值为int的默认值0,然后解析,最后进入到初始化阶段把10赋给a。
-
-
类加载的时机
- 创建类的实例,即new一个对象
- 访问一个类或者接口的静态变量、静态方法,或者对其静态变量赋值,会加载该类
- 反射
- 初始化一个类的子类(首先初始化子类的父类)
- JVM启动时标明的启动类
类加载器详解
-
类加载器:负责加载所有的类,为所有被载入内存中的类生成一个java.lang.Class实例对象,一但一个类被加入JVM中,同一个类就不会被再此载入了。其中一个类以自己的全限定类名作为标识。
- JVM预定义有三个类加载器
- 启动类加载器(根类、引导类加载器):用来加载Java的核心类(JAVA_HOME\lib目录),用C++实现,是虚拟机自身的一部分。不继承java.lang.ClassLoader
- 扩展类加载器:负责加载JRE的扩展目录(JAVA_HOME\lib\ext目录)
- **应用程序类加载器:负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path属性。**如果没有特别指定,用户自定义的类加载器都以此类作为父加载器。
- 类加载器加载Class大概需要以下八个步骤:
- 检测该Class文件是否已经加载过,如果加载了直接进入第8步。
- 如果没有父类加载器(要么其上层是启动类加载器,要么本身就是启动类加载器),跳转到第4步,否则执行第3步。
- 请求使用父类加载器去载入目标类,如果载入成功则跳到第8步,否则执行第5步。
- 请求使用启动类加载器去载入目标类,成功则跳至第8步,否则第跳到第7步。
- 当前类加载器尝试寻找Class文件,成功则执行第6步,否则跳到第7步。
- 从文件中载入Class,成功后跳到第8步。
- 抛出ClassNotFountException异常。
- 返回对应的java.lang.Class对象。
- JVM预定义有三个类加载器
三种类加载机制
- 全盘负责:一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 双亲委派:先让父类加载器试图加载该Class,只有父类加载器无法加载该类时才尝试从自己的类路径中加载该类,依次递归,请求最终将达到顶层的启动类加载器。
- Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载。
- 安全问题,假如通过网络传递了一个名为java.lang.Integer的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心Java API中发现该这个名字的类,发现该类已经被加载,这样就不会重复的加载网络传递过来的这个类了,而是返回已经加载过的Integer.class,这样可以防止核心API库被随意修改。
- 双亲委派的破坏:某些情况下父类加载器需要委托子类加载器去加载class文件,受到加载范围的限制,父类加载器无法加载到需要的class文件。具体破坏可以自定义类加载器,重写loadClass方法;或者是使用线程上下文类加载器。
- 缓存机制:保证所有加载过的Class都会被缓存,当程序中需要使用某个class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会去读取二进制文件,并将其转换为Class对象存入缓存区中。
如何自定义类加载器
- 继承ClassLoader类(这个是Application ClassLoader类)
- 重写findClass(String className)方法
- 为来自本地文件系统或者其他来源的类加载其字节码;
- 调用ClassLoader父类的defineClass方法,向虚拟机提供字节码。
ClassLoader父类的loadClass方法用于将类的加载操作委托给其父类加载器去进行,只有该类尚未加载且父类加载器也无法加载该类时,才调用findClass方法。
泛型与泛型擦除
泛型:允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可以称为类型实参)。在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
类型通配符:
- ?表示不确定的Java类型,常用就是表示上下限
- T表示具体的一个Java类型
- KV表示Java键值中的Key和Value
- E代表元素
泛型擦除
C#的List和List是两个不同的类型,在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
Java的泛型只在程序源码中存在,在编译后的字节码文件中就已经替换为原来的原生类型,并且在相应的地方法插入了强制转型代码。List和List就是同一个类型。这叫类型擦除,是伪泛型。不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参。
public static void method(List<Integer> integers) {System.out.println("Integer");
}public static void method(List<String> integers) {System.out.println("String");
}
上述代码不能通过编译,原因就是参数List<Integer>和List<String>编译之后都被擦除了,变成了原生类型List<E>,擦除使得方法签名变得一模一样。
然而下面的代码,在jdk1.5以前是可以通过编译的(1.8无法通过编译):
public static int method(List<Integer> integer) {System.out.println("integer");return 1;
}public static String method(List<String> integers) {System.out.println("string");return "";
}
泛型的缺点
主要是针对通配符的,Test中的T不能带入int/float等Java默认的基本类型,且T不能是会抛出异常的类。
如果有一个类Test<A, B> ,里面有一个函数 A f()就不能在写B f()了,因为泛型擦除导致了方法重复。
运行期间无法获取泛型的类型信息,因为泛型都被擦除了,被替换成了原生类型。
从方法调用深入理解重载和重写
静态分派与动态分派
-
Class文件的编译过程中不含传统编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。这就使得Java方法调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。
-
静态分派:所有依赖静态类型来定位方法执行版本的分派动作。
public class StaticDispatch {private static abstract class Human {}private static class Man extends Human {}private static class Woman extends Human {}private void sayHello(Human human) {System.out.println("hello, human");}private void sayHello(Man man) {System.out.println("hello, man");}private void sayHello(Woman woman) {System.out.println("hello, woman");}public static void main(String[] args) {//Human称为变量的静态类型,Man称为变量的实际类型//静态类型的变化只在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的//实际类型变化的结构在运行期才可以确定,编译期在编译程序的时候并不知道一个对象的实际类型是什么Human man = new Man();Human woman = new Woman();StaticDispatch sd = new StaticDispatch();//确定对象为sd的前提下,使用哪个重载版本完全取决于传入参数的数量和数据类型//编译器在重载时通过参数的静态类型而不是实际类型作为判断依据,静态类型是编译期可知的。//所以在编译阶段,javac编译器会根据参数的静态类型而不是实际类型作为判断依据sd.sayHello(man);sd.sayHello(woman);} } 输出结果为: hello, human hello, human
静态分派典型应用就是方法重载。静态分派发生在编译阶段,故不是虚拟机执行,而是编译器执行。编译器虽然能确定方法的重载版本,但并不唯一,基本上确认的是一个更加适合的版本。
自动转型是一个自底而上的过程:char-》int-》long-》float-》double。但不会发生char到byte或者short(不安全) -
动态分派:在运行期根据实际类型确定方法执行版本的分派过程,这个过程是由虚拟机执行的。
- 重写机制的本质:一条invokevirtual指令
- 找到操作数栈的第一个元素所指向的对象的实际类型,记作C
- 如果C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束。否则返回java.lang.IllegalAccessError
- 否则按照继承关系从下往上依次对C的各个父类进行第二步都搜索和验证过程
- 始终没有合适的方法,则抛出java.lang.AbstractMethodError异常
- 方法的调用是确定对象的实际类型,所以每次调用该指令都会把常量池中的类方法符号引用解析到不同的直接引用上。
- 动态分配的实现:
- 动态分派非常频繁,且方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法。基于性能考虑不会真正的进行如此频繁的搜索。
- 具体实现为:在方法区中建立一个虚方法表,使用虚方法表索引来替代元数据查找以提高性能。里面存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果重写了,则子类方法表的地址将会替换为指向子类实现版本的入口地址。
- 重写机制的本质:一条invokevirtual指令
重载
- 让类以同一的方式去处理不同类型数据的一种手段。多个同名函数同时存在,具有不用的参数个数/类型。**重载是一个类中多态性的一种表现,是编译时多态。**Java的方法重载,就是在类中可以创建多个方法,他们具有相同的名字,但具有不同的参数和不同的定义。调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法。重载的时候,方法名要一样,但参数的类型和个数一定要不一样。返回值可一样可不一样,但是无法只通过返回值作为重载函数的区分标准。
- 重载遵循的规则:
- 方法名相同,形参列表必须不同
- 返回值类型可以相同也可以不相同
- 只有返回值类型不同不能叫做方法的重载
重写
-
父类与子类之间的多态性,是运行时的多态性,表现在子类对父类的方法进行重新定义。如果在子类中定义某方法与其父类有相同的名称和参数,我们说父类的该方法被重写。子类可以直接拿父类的方法来用,也可以对其进行改造。若子类中的方法与父类中的某一方法有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法。如果需要使用父类中原有的方法,可以使用super关键字,该关键字引用了当前类的父类。
-
重写遵循三同一小一大原则:
- 方法名相同,形参列表相同,返回值类型相同。
- 子类方法声明抛出的异常比父类方法声明抛出的异常要小。
- 子类方法的访问修饰符应该比父类方法更大或相等。
重载与重写的区别
- 重载是静态分派的应用,发生在编译期,由编译器进行执行;重写则是动态分派的应用,在运行期根据实际类型确定方法执行版本的分派过程,由虚拟机执行
Java的IO流
同步与异步:关注消息通知机制
- 同步:发起一个调用后,被调用者未处理完请求之前,调用不返回。一旦返回就直接得到返回值
- 异步:发起一个调用后,立刻得到被调用者的回应表示已经接收到请求(被调用者并没有返回结果),此时我们可以处理其他请求。被调用者通常依靠事件、回调机制等通知调用者返回器结果。
- 例子:去买一本书,同步就是老板告诉你“我现在给你找,你等一下”。这个等可能是5秒也可能是一天。而异步就是“你先回去,我找到通知你”。
- 两者的最大区别就是异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者返回结果。性能较好
阻塞和非阻塞:关注等待调用结果时的状态
- 阻塞:阻塞就是发起一个请求,调用者会被挂起并一直等待该请求结果返回。此期间不能从事其他任务,得到返回才能继续干其他的事情。
- 非阻塞:非阻塞就是发起一个请求,调用者不会被挂起,不用一直等着结果返回,可以先去干其他的事情。
- 例子:还是买书,阻塞就是干等,而非阻塞就是可以干其他的事情,比如玩手机
流的分类
- 按照流的流向分,可以分为输入流和输出流。
- 按照操作单元分,可以分为字节流和字符流。
- 按照流的角色分,可以分为节点流和处理流。
- InputStream/Reader是所有输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer是所有输出流的基类,前者是字节输出流,后者是字符输出流。
字节流和字符流
- 使用:字节流比较通用。如果是文本文件通常使用字符流,而图片、视频、音频等都是二进制数据使用字节流。
- 区别:字节流的操作不会经过缓冲区(内存)而是直接操作文件本身的,而字符流的操作会先经过缓冲区(内存)然后通过缓冲区再操作文件。
- 其中,对于对象的读写是两个类完成的:
- ObjectOutputStream:提供序列化对象并把其写入流的方法
- ObjectInputStream:读取流并反序列化对象
什么是缓冲区
- 缓冲区是一段特殊的内存区域,当程序需要频繁的操作一个资源(如文件后者数据库),会导致性能变得很低。为了提升性能就可以将一部分数据暂时读写到缓冲区,以后直接从此区域读写数据即可,这样就显著的提升了性能。字符流的操作都是在缓冲区操作的,flush()方法可以主动将缓冲区刷新到文件。
BIO vs NIO vs AIO
- Bolcking IO:同步阻塞IO模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数(小于单机1000)不是特别高的情况下,这种模型是比较好的,可以让每一个连接专注于自己的IO并且编程模型也比较简单,不用过多的考虑系统的过载、限流等问题。
- New(No-Blocking) IO:同步非阻塞IO模型,Java1.4引入,对应java.nio包,提供了Channel、Selector、Buffer等抽象。
- NIO是不阻塞的:单线程从channel中读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中,线程再继续处理数据。写数据也是如此。
- **NIO面向缓冲区buffer:buffer是一个对象,它包含一些要写入或者要读出的数据。NIO中所有的数据都是用缓冲区处理的,在读数据时它直接读到缓冲区,在写数据时写入到缓冲区中,任何时候访问NIO中的数据都是通过缓冲区进行操作。**目的是为了减少IO操作,提升性能。
- NIO是通过channel进行读写的。channel是双向的,可读也可以写。从channel读取数据:创建一个缓冲区,然后请求通道读取数据;从channel写入数据:创建一个缓冲区,填充数据并要求通道写入数据。
- Asynchronous IO:异步非阻塞IO模型,Java7中引入。其基于时间和回调机制,操作后直接返回,不会一直等待结果,当后台处理完成后,操作系统会通知相应的线程进行后续操作。
JDK1.8新特性
- Lambda表达式:本质上是一段匿名内部类,也可以是一段可以传递的代码。
- 函数式接口。
- 只定义了一个抽象方法的接口(除了Object类中的public),并且还提供了了注解@FuncationalInterface
- 常见的有四种
- Consumer:消费型接口,有参无返回值。
- Supplier:供给型接口,无参有返回值。
- Function<T, R>:函数式接口,有参有返回值。
- Predicate :断言型接口,有参有返回值,返回值是boolean类型。
- 加入了流式的API。
- 创建stream
- 中间操作(过滤、map)
- 终止操作
- 方法引用。
- 对象::实例方法名
- 类::静态方法名
- 类::实例方法名
- 接口中可以有default方法和静态方法。
- 新时间日期API:LocalDate、LocalTime、LocalDateTime。这些日期API都是不可变的,可以安全的用于多线程。
- 使用Optional类来快速定位NPE,并可以在一定程度上减少参数非空检验的代码量。
- 使用数组+链表+红黑树实现HashMap,且链表由头插变为尾插。
- 移除永久代,取而代之的是元空间。
- ConcurrentHashMap从分段加锁变成了由synchronized和CAS来保证线程安全。
JDK、JRE、JVM的区别
- JDK:Java Development Kit,Java支持工具,是提供给Java开发人员使用的,其中包含了Java的开发工具,包括了JRE。里面包含java.exe(运行Java的程序)和javac.exe(编译工具生成.class)等。
- JRE:Java Runtime Environment:Java运行环境,包括JDK和Java程序所需的核心类库等。核心类库主要是java.lang:其包含了运行Java程序必不可少的系统类。
- JVM:Java Virtual Machine,Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java可以实现跨平台。
如何理解Java是跨平台的
Java编写的程序一次编译后可以再多个系统平台上运行,原理:Java程序通过JVM在系统平台上运行的,只要该系统可以安装相应的JVM就可以运行Java程序。
Java源代码经过JVM编译器编译后产生的文件(即.class文件)只面向JVM。即机器和编译程序之间加入了一层抽象的虚拟机器,这个虚拟机在任何平台上都提供给编译程序一个共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码就叫做字节码(.class文件),他不面向特定的处理器,只面向虚拟机。其中,不管程序之前是什么编码形式,在转成.class之后,都是用Unicode编码表示。
**一个Java源代码的执行过程:**Java源代码-》编译器-》JVM可执行的Java字节码(虚拟指令,也就是.class)-》类加载器-》字节码校验器-》JVM中的解释器-》机器可执行的二进制机器码-》程序运行。
面试题:在Java中定义一个空参数方法体的构造方法的作用
Java程序在执行子类的构造方法之前,如果没有用super来调用父类特定的构造方法,则会调用父类的没有参数的构造方法。因此如果父类中只定义了有参的构造方法,而子类构造方法中又没有用super去显示的调用特定的构造方法,则会发生编译错误,因为Java程序在父类中找不到默认的无参构造方法。
面试题:Maven冲突怎么办
maven冲突就是指:依赖时使用maven坐标来定位,而maven坐标主要由gav构成,两个包只要这三个一个不同,maven就认为是不同的。依赖会传递,如果A依赖了B,B依赖了C,那么A的依赖中就会出现B和C。maven对同一个groupId和artifactId的冲突制裁不是以version越大就保留,而是以近的进行保留。且依赖的scope会影响依赖的问题。
方案一:在出现冲突的依赖段里面加入exclusions来让其忽略其依赖的某个包。
<exclusions><exclusion><artifactId>spring</artifactId><groupId>org.springframework</groupId></exclusion>
</exclusions>
方案二:使用dependencyManagement来锁定jar版本
面试题:git和SVN的区别
- git是分布式版本控制系统;而SVN是集中式版本控制系统,所有人都修改的是服务器上的程序。
- git把内容按元数据方式存储,而SVN是按照文件。
- git是将项目缓存在本地再推送到服务器,而svn是直接与服务器进行交互。
- git可以不用联网开发,而svn必须在联网的情况下工作。
- svn旨在项目管理,而git旨在代码管理。
面试题:throw vs throws
throws用来声明一个方法可能会产生的所有异常,不做任何处理而是将异常抛出。
- 用来方法声明后面,跟的是异常类名。
- 可以跟着多个异常,用逗号隔开。
- 表示抛出异常,由抛出异常的方法调用者自行处理。
- 只是表示异常的可能性,并不一定会发生。
throw则是用来抛出一个具体的异常类型。
- 用在方法体内,跟的是异常对象名。
- 只能抛出一个异常对象名。
- 表示抛出异常,由方法体内的语句处理。
- throw显示的抛出异常,一旦执行则一定抛出了某种异常。
拓展:异常继承关系
Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身可以处理,错误是无法处理。
- Error(错误):程序无法处理的错误,表示运行应用程序中比较严重的问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM出现的问题。常见的有VirtualMachineError、OutOfMemoryError、StackOverflowError、NoClassDefFoundError等。
- Exception(异常):程序本身可以处理。一个重要子类是RuntimeException,意思就是运行时异常,其和其子类都是一般由我们自己编码造成的。
- 运行时异常:RE以及其子类,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。一般都是由自己的程序逻辑错误引起的。运行时异常的特点就是:Java编译器是不会检查出这些异常的,编译是可以通过的。
- 非运行时异常:RE以外的异常,从程序上讲是必须处理的异常,如果不处理(显示抛出或者try catch),程序就不能编译通过。
面试题:NoClassDefFoundError和ClassNotFoundException的区别
- NCDFE是一种Error,产生原因是JVM或者ClassLoader在运行时,类加载器在classpath下找不到需要的类定义(编译期可以正常找到),这时候虚拟机就会抛出该错误。通常造成该错误的原因是打包过程中漏掉了部分类,或者jar包出现损坏或篡改,对应的Class在classpath中不可用等原因。
- CNFE是Exception,当应用程序运行的过程中尝试使用类加载器去加载Class文件的时候,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。大多时候我们使用了Class.forName()方法动态的加载类信息,但是这个类在路径中并没有被找到,比如数据库连接,就会直接抛出该异常。
- 区别:
- CNFE强调的是动态加载Class找不到该类会抛出该异常;NCDFE强调的是编译成功以后运行过程中Class找不到而导致抛出错误。
- CNFE一般是跟我们代码有关,而NCDFE是由JVM运行时系统抛出。
自定义注解怎么做?
元注解@interface上面需要注解上一些东西,包括@Retention、@Target、@Document、@Inherited四种。
- @Retention:注解的保留策略
- @Target:注解的作用目标
- @Document:注解包含在javadoc中
- @Inherited:注解可以被继承
public @interface Init {//do someting
}
ps:注解的底层实现也是反射。