1. JDK、JRE、JVM 之间的关系?
- JDK(Java Development Kit):是 Java 开发工具包,是整个 Java 的核心,包括了 Java 运行环境 JRE、Java 工具和 Java 基础类库。
- JRE(Java Runtime Environment):Java 运行环境。
JVM(Java Virtual Machine):Java 虚拟机,是整个 Java 实现跨平台的最核心的部分。所有的 Java 程序会首先被编译为 .class 类文件,这种类文件可以在 Java 虚拟机上运行。
2. Java 中的八大基本类型
整数型:byte、short、int、long
- 浮点型:float、double
- 字符型:char
- 布尔型:boolean
3. Java 代码是如何被执行的?
Java 语言中有两种编译器,其中 javac 是前端编译器,JIT 是后端编译器。在 Java 中提到『编译』,自然很容易想到 Javac 编译器将*.java
文件编译成为*.class
文件的过程,这里的 Javac 编译器称为前端编译器。相对应的还有后端编译器,它在程序运行期间将字节码转变成机器码(现在的 Java 程序在运行时基本都是解释执行加编译执行),如 HotSpot 虚拟机自带的 JIT(Just In Time Compiler)编译器(分 Client 端和 Server 端)。但是 Java 的 HotSpot 虚拟机的机制是直到有热点方法被执行 10000 次才会触发后端的 JIT 编译,而其他的方法运行在解释模式下,以避免出现 JIT 编译花费的时间比方法解释执行消耗的时间还要多的情况。
总结一下,Java 程序从源代码到运行一般有以下两步:
- 前端编译器 Javac 将
*.java
文件编译成*.class
文件。 JVM 将
*.class
文件转变成机器码。这里需要注意的是,JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式执行速度会相对比较慢。因为有些代码块是经常要被调用的(热点代码),所以后面引进了 JIT 编译器来解决热点代码编译的问题,而 JIT 属于运行时编译,当 JIT 编译器第一次完成编译后,会将字节码对应的机器码保存下来,下次可以直接使用。4. Java 类内部代码执行顺序
执行父类的静态代码块
- 执行子类的静态代码块
- 执行父类的构造代码块
- 执行父类的构造函数
- 执行子类的构造代码块
- 执行子类的构造函数
如果既对字段进行初始化,又在构造方法中对字段进行初始化的话,那么按照如下顺序进行初始化:
- 先初始化字段,例如,int age = 10;表示字段初始化为10,double salary;表示字段默认初始化为0,String name;表示引用类型字段默认初始化为null;
- 执行构造方法的代码进行初始化。
因此,构造方法的代码由于后运行,所以最终字段中的值由构造方法的代码确定。
5. Java 中的值传递
在 Java 中所有的参数传递,不管基本类型还是引用类型,都是值传递。
如果参数是基本类型,传递的是基本类型的字面量值的拷贝。 如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。
6. 静态变量是什么?
静态变量是被 static 修饰的变量,也被称为类变量。它属于类,不管创建多少个对象,静态变量在内存中有且仅有一个拷贝。所以静态变量可以实现让多个对象共享内存。
7. Error 和 Exception 的区别?
Error 类和 Exception 类的父类都是 Throwable 类。主要区别如下:
- Error: 一般是指与虚拟机相关的问题。
- Exception:分为运行时异常和非运行时异常。
- RuntimeException:不可预知的,程序应该自行避免(如数组下标越界,访问空指针等)
- 非RuntimeException:可预知的,从编译器校验的异常(如 IOException、SQLException 等)
Throw 和 Throws 的区别:Throw 用于方法内部,Throws 用于方法声明上。Throw 后跟异常对象,Throws 后跟异常类型。
8. 什么是 Lambda 表达式
Lambda 表达式是 JDK8 的一个新特性,它允许将函数作为参数传递进方法中,可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。
9. 什么是注解
注解的本质是一个接口,该接口默认继承 Annotation 接口,使用 @interface 进行定义。注解主要有三类:元注解、自定义注解以及框架定义的注解。
- 接口里面的成员方法称为注解的属性
- 定义了属性,要在使用的时候给属性赋值
- 如果定义属性时使用default关键字给属性默认初始值,则可以不进行赋值
- 若只有一个属性且名为value,则使用时可以直接写值
数组赋值时使用 { } 包裹,数组只有一个值时可以不用 { }
10. 深拷贝和浅拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递的拷贝。
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容。进行深拷贝常用的方法有两种:
1. 序列化(serialization)这个对象,再反序列化回来,就可以得到这个新的对象,只是这个序列化的规则需要我们自己来写。
2. 利用 clone() 方法。11. 泛型
Java 泛型(generics)简单的说就是在创建或调用方法的时候才明确具体的类型。11.1 泛型的好处
在没有泛型的出现之前,我们通常是使用类型为 Object 的元素对象。比如我们可以构建一个类型为 Object 的集合,该集合能够存储任意数据类型的对象,但是我们从集合中取出元素的时候我们需要明确的知道存储每个元素的数据类型,这样才能进行元素转换,不然会出现ClassCastException
异常。
Object 是所有类的父类,任何类的对象都可以设置给该 Object 类引用变量,使用的时候可能需要强制类型转换,但是使用了泛型 T、E 这些标识符后,在使用之前类型就已经确定了,这样就可以不需要进行强制类型转换了。
泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
public class Generic<T> {
private T t;
public void setT(T t) {
this.t = t;
}
public T getT() {
return t;
}
public static void main(String[] args) {
// 不指定类型
Generic generic = new Generic();
generic.setT("test");
String test = (String) generic.getT(); //不使用泛型,则需要强制类型转换
System.out.println(test);
// 指定类型
Generic<String> generic1 = new Generic<>();
generic1.setT("test1");
String test1 = generic1.getT();
System.out.println(test1); //使用泛型,不需要强制类型转换
}
}
如上面的代码所示,使用泛型之后,省去了强制转换,可以在编译时检查类型安全。
11.2 泛型中的通配符
本质上T,E,K,V,?
都是通配符,没什么区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?
是这样约定的:
- E:Element(元素,集合中使用,特性是枚举)
- T:Type(表示一个具体的 Java 类型)
- K:Key(键)
- V:Value(值)
- N:Number(数值类型)
- ?:表示不确定的 Java 类型
11.3 类型擦除
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。 ```java Listl1 = new ArrayList (); List l2 = new ArrayList ();
System.out.println(l1.getClass() == l2.getClass());
上面的代码打印的结果为 true,是因为`List<String>`和`List<Integer>`在 JVM 中的 Class 都是 List.class。
<a name="4QPVu"></a>
# 12. serialVersionUID 有什么用?
serialVersionUID 适用于 Java 的序列化机制。简单来说,Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 InvalidCastException。
具体的序列化过程是这样的:序列化操作的时候系统会把当前类的 serialVersionUID 写入到序列化文件中,当反序列化时系统会去检测文件中的 serialVersionUID,判断它是否与当前类的 serialVersionUID 一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。
<a name="FrZCg"></a>
# 13. Java 中如何判断两个对象是否相等?
如何判断两个对象相等,这个问题实际上可以看做是如何对 equals 方法和 hashCode 方法的理解。下面我们通过三个子问题循序渐进地彻底理解这个问题。
<a name="WBoPf"></a>
## 13.1 equals 方法和 == 的区别
1. equals 是方法,而 == 是操作符。
1. 对于基本类型的变量(如 short、 int、 long、 float、 double),只能使用 == ,因为这些基本类型的变量没有 equals 方法。对于基本类型变量的比较,使用 == 比较, 一般比较的是它们的值。
1. 对于引用类型的变量(例如 String 类)才有 equals 方法,因为 String 继承了 Object 类, equals 是 Object 类的通用方法。默认情况下,也就是没有复写 Object 类的 equals 方法,使用 == 和 equals 比较是一样效果的,都是比较的是它们在内存中的存放地址。但是对于某些类来说,为了满足自身业务需求,可能存在 equals 方法被复写的情况,这时使用 equals 方法比较需要看具体的情况,例如 String 类,使用 equals 方法会比较它们的值。此时 equals 方法通常用于比较两个对象的内容是否相等,== 通常用来比较两个对象的地址是否相等。
其中第 3 点理解起来有点复杂,因为这里的 equals 方法需要分两种情况来讨论,一种情况是该方法没有被重写,另外一种是该方法被重写。
- 对于 equals 方法没有被重写的情况。则使用 equals 方法比较两个对象时,相当于 == 比较两个对象的地址是否相等。地址相等,返回 true,地址不相等,返回 false。以下是 Object 类中的 equals 方法的源码:
```java
public boolean equals(Object obj) {
return (this == obj);
}
- 对于 equals 方法被重写的情况。equals 方法只要重写为能够判断两个对象中的内容相等即可。内容相等,返回 true,内容不相等,返回 false。
13.2 hashCode 方法和 equals 方法的联系
equals 方法的作用是判断两个对象是否相等,equals 方法在没有重写时可以判断两个对象的地址是否相等,equals 方法重写后可以判断两个对象的内容是否相等。
hashCode 的作用是用来获取哈希码,实际返回值为一个 int 型数据,用于确定对象在哈希表中的位置。Object 类中有一个 hashCode 本地方法,这意味着所有类都有 hashCode 方法。
但是,hashcode 方法只有在创建某个类的散列表的时候才有用,需要根据 hashcode 值确认对象在散列表中的位置(常见的散列表如 HashMap,HashSet,HashTable),在其他情况下没用。所以,如果一个对象一定不会在散列表中使用,那么是没有必要复写 hashCode 方法的。但一般情况下我们还是会复写 hashCode 方法,因为谁能保证这个对象不会出现在 hashMap 中呢?
- 在重写了 equals 方法后,但没有重写 hashCode 方法时,会出现两个对象 equals 方法返回 true,但 hashCode 值不一定相等的情况。
- 两个对象的 hashCode 值相等,两个对象不一定相等。判断两个对象是否相等,需要判断 equals 方法是否返回 true。
13.3 为什么重写 equals 方法就一定要重写 hashCode 方法?
这个问题应该有个前提,就是我们需要用到散列表如 HashMap、HashSet、HashTable 时,这时候重写 equals 方法才一定要重写 hashCode 方法。如果用不到散列表的话,其实不重写 hashCode() 也可以。但一般情况下我们还是会复写 hashCode 方法,因为谁能保证这个对象不会出现在 hashMap 中呢?因此,这个问题其实更多的是一种契约。
以稍微简单一些的 HashSet 为例,HashSet 在添加一个元素的时候,HashSet 会先计算对象的 hashCode 值来获得对象加入的位置(这个位置上可能存在若干个 hashCode 值相等的对象),如果发生了哈希碰撞,再调用 equals 方法来检查 hashCode 相等的对象在内容上是否相等。如果此时 equals 方法返回 true,那么不会插入;如果 equals 方法返回 false,那么会插入。
如果我们重写了 equals 方法后没有重写 hashCode 方法,那么在插入两个内容相等的对象到 HashSet 中的时候,因为这两个对象的 hashCode 不相同,所以尽管这两个对象 equals 判断为 true,这两个对象仍然会添加进 HashSet 中,这样就会出现两个相等的对象同时出现在 HashSet 中的现象。
在 Object 类中的 equals 方法的注释上有这么一段:Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.
同时在 Object 类中的 hashCode 方法的注释上有这么一段:If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result. 因此,在 Java 的设计之初,就表明在重写的 equals 方法的时候必须重写 hashCode 方法。
14. 为什么Java中的main方法必须是public static void的?
在 Java 中,main 方法是 Java 应用程序的入口方法。程序运行时,要执行的第一个方法就是 main 方法。在使用 Java 写下第一个 hello world 的时候,我们需要创建一个 main 方法,当我们使用 Spring Boot 启动一个 web 应用的时候,我们也同样需要一个main方法。
14.1 Java虚拟机如何启动
在《Java语言规范》中,对于 Java 虚拟机的启动给出了明确的定义:Java 虚拟机是通过加载指定的类,然后调用该类中的 main 方法而启动的。也就是说,通过调用某个指定类的 main 方法,传递给他单个的字符串数组参数,就可以启动 Java 虚拟机。
一个 main 方法想要被执行,需要经过几个步骤,首先对应的类需要被虚拟机加载,然后需要进行链接和初始化、之后才是调用 main 方法。那么一个方法想要被调用,根据他的访问限定符以及方法类型不同,被调用的条件也是不同的。
14.2 为什么 main 方法是公有的(public)?
Java 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- private : 在同一类内可见。使用对象:变量、方法。注意:不能修饰类(外部类)
- public : 对所有类可见。使用对象:类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。注意:不能修饰类(外部类)。
以上四种控制符都可以用来修饰方法,但是被修饰的方法的访问权限就不同了。而对于 main 方法来说,我们需要通过 JVM 直接调用他,那么就需要他的限定符必须是 public 的,否则是无法访问的。
14.3 为什么 main 方法是静态的(static)?
static 是静态修饰符,被他修饰的方法我们称之为静态方法,静态方法有一个特点,那就是静态方法独立于该类的任何对象,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载,Java 虚拟机就能根据类名在运行时数据区的方法区内定找到他们。
而对于 main 方法来说,他的调用过程是经历了类加载、链接和初始化的。但是并没有被实例化过,这时候如果想要调用一个类中的方法。那么这个方法必须是静态方法,否则是无法调用的。
14.4 为什么 main 方法没有返回值(void)?
如果大家对于C语言和C++语言有一定的了解的话,就会知道,像 C、C++ 这种以 int 为 main 函数返回值的编程语言。
这个返回值在是程序退出时的 exit code,一般被命令解释器或其他外部程序调用已确定流程是否完成。一本正常情况下用 0 返回,非 0 为异常退出。
而在 Java 中,这个退出过程是由 JVM 进行控制的,在发生以下两种情况时,程序会终止其所有行为并退出:
- 所有不是后台守护线程的线程全部终止。
- 某个线程调用了 Runtime 类或者 System 类的 exit 方法,并且安全管理器并不禁止 exit 操作。
上面的两种情况中,第二种情况一旦发生,JVM 是不会管 main 方法有没有执行完的,他都会终止所有行为并退出,这时候 main 方法的返回值是没有任何意义的。所以,main 的返回值就被固定要求为 void。
14.5 为什么 main 方法的入参是字符串数组(String[])
Java 应用程序是可以通过命令行接受参数传入的,从命令行传递的参数可以在 Java 程序中接收,并且可以用作输入。因为命令行参数最终都是以字符串的形式传递的,并且有的时候命令行参数不止一个,所以就可能传递多个参数。这时候,作为 Java 应用程序执行的入口,main 方法就需要能够接受这多个字符串参数,那么就使用字符串数组了。
14.6 总结
main 方法是 JVM 执行的入口,为了方便 JVM 调用,所以需要将他的访问权限设置为 public,并且静态方法可以方便 JVM 直接调用,无需实例化对象。
因为 JVM 的退出其实是不完全依赖 main 方法的,所以 JVM 并不会接收 main 方法的返回值,所以给 main 方法定义一个返回值没有任何意义。所以 main 方法的返回值为 void。
为了方便 main 函数可以接受多个字符串参数作为入参,所以他的形参类型被定义为 String[]。