JDK 和 JRE、JVM

区别

  1. JRE:Java 运行环境,提供了 Java 运行所需的环境,包含了 JVM、核心类库和其他支持运行 Java 程序的文件
  2. JDK:Java 开发工具包,提供了 Java 的开发环境和运行环境。JDK 包含了 JRE,如果只运行 Java 程序,安装 JRE 即可,要编写 Java 程序需安装 JDK
  3. JVM:Java 虚拟机,是整个 Java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的软件程序。所有的 Java 程序会首先被编译为.class 的类文件,这种类文件可以在虚拟机上运行

字节码

在 Java 中,JVM 可以理解的代码叫做字节码(即扩展名为.class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方法,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以,Java 程序运行时相对来说还是高效的(不过,和 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无需重新编译便可在多种不同操作系统的计算机上运行

JIT

.class-> 机器码,在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(just-in-time compilation)编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用

引用和对象

  1. new Hero();:代表创建了一个 Hero 对象,但是也仅仅是创建了一个对象,没有办法访问它。
  2. Hero h = new Hero();:为了访问这个对象,会使用引用来代表这个对象,h 这个变量是 Hero 类型,又叫做引用。= 指的是 h 这个引用代表右侧创建的对象,在面向对象里,又叫做“指向”
  3. 一个对象引用可以指向 0 个或 1 个对象;一个对象可以有 n 个引用指向它
  4. 对象的相等一般比较的是内存中存放的内容是否相等
  5. 引用相等一般比较的是他们指向的内存地址是否相等

创建对象的方法

  1. 使用 new 关键字
  2. 使用 Class 类的 newInstance 方法,该方法调用无参的构造器创建对象(反射):Class.forName.newInstance()
  3. 使用 clone()方法
  4. 反序列化,例如:调用 ObjectInputStream 类的 readObject()方法

值传递和引用传递

  1. 值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量
  2. 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本,并不是原对象本身,在方法中对其值进行改变的时候,他的地址没有变,值也就跟着改变了,对引用对象进行操作会同时改变原对象

注意:Java 内的传递都是值传递

传递基本类型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}

public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}

a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身

传递引用类型参数 1

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}

public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}

传递引用类型参数 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person {
private String name;
// 省略构造函数、Getter&Setter方法
}

public static void main(String[] args) {
Person xiaoZhang = new Person("小张");
Person xiaoLi = new Person("小李");
swap(xiaoZhang, xiaoLi);
System.out.println("xiaoZhang:" + xiaoZhang.getName());
System.out.println("xiaoLi:" + xiaoLi.getName());
}

public static void swap(Person person1, Person person2) {
Person temp = person1;
person1 = person2;
person2 = temp;
System.out.println("person1:" + person1.getName());
System.out.println("person2:" + person2.getName());
}

swap 方法的参数 person1 和 person2 只是拷贝的实参 xiaoZhang 和 xiaoLi 的地址。因此,person1 和 person2 的互换只是拷贝的两个地址互换罢了,并不会影响到实参 xiaoZhang 和 xiaoLi

克隆

为什么要克隆?

克隆的对象可能包含一些已经修改过的属性,而 new 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠 clone 方法了,那么把这个对象的临时属性一个一个赋值给我新 new 的对象不也行吗?可以,但是操作麻烦,且 clone 是一个 native 方法,在底层实现的,效率快

如何实现对象的克隆?

  1. 实现 Cloneable 接口(为标记接口),重写 Object 类中的 clone()方法(浅克隆)
  2. 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆(深克隆)

深克隆和浅克隆

  1. 浅克隆:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Student{
private Integer id;
private String name;

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}

public Student(Integer id, String name) {
this.id = id;
this.name = name;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class Person implements Cloneable {
private Integer id;
private String name;
private Student student;

@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", student=" + student +
'}';
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Student getStudent() {
return student;
}

public void setStudent(Student student) {
this.student = student;
}

public Person(Integer id, String name, Student student) {
this.id = id;
this.name = name;
this.student = student;
}

@Override
protected Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}


public static void main(String[] args) throws CloneNotSupportedException {
Student stu = new Student(1, "stu");
Person per1 = new Person(1, "per", stu);
Person per2 = per1.clone();
System.out.println(per1);
System.out.println(per2);
System.out.println(per1.getStudent().getClass()==per2.getStudent().getClass());

per1.setName("person");
Student stu2 = per1.getStudent();
stu2.setName("stu2");
System.out.println(per1);
System.out.println(per2);
System.out.println(per1.getStudent().getClass()==per2.getStudent().getClass());
}
}

/*
结果为:
Person{id=1, name='per', student=Student{id=1, name='stu'}}
Person{id=1, name='per', student=Student{id=1, name='stu'}}
true
Person{id=1, name='person', student=Student{id=1, name='stu2'}}
Person{id=1, name='per', student=Student{id=1, name='stu2'}}
true
*/

注意:克隆后的值变量会开辟新的内存地址,克隆对象修改值不会影响原来对象。引用类型只会存在一份内存地址,执行 Object 的 clone 方法拷贝的也是引用的复制(这部分的内存空间不一样),但是引用指向的内存空间是一样的,原对象修改变量或者浅拷贝修改引用变量都会引起双方的变化

  1. 深克隆:拷贝对象和原始对象的引用类型引用不同对象。深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Student implements Cloneable {
private String name;
private int age;
public Student(String name, int age){
this.name = name;
this.age = age;
}

@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Teacher implements Cloneable{
private String name;
private int age;
private Student student;

public Teacher(String name, int age, Student student){
this.name = name;
this.age = age;
this.student = student;
}
// 覆盖
@Override
public Object clone() {
Teacher t = null;
try {
t = (Teacher) super.clone();
t.student = (Student)student.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return t;
}
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public Student getStudent() {
return student;
}

public void setStudent(Student student) {
this.student = student;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class test {
public static void main(String[] args) {
Student s = new Student("学生1", 11);
Teacher origin = new Teacher("老师原对象", 23, s);
System.out.println("克隆前的学生姓名:" + origin.getStudent().getName());
Teacher clone = (Teacher) origin.clone();
// 更改克隆后的学生信息 更改了姓名
clone.getStudent().setName("我是克隆对象更改后的学生2");
System.out.println("克隆后的学生姓名:" + clone.getStudent().getName());
}
}

/*
结果:
克隆前的学生姓名:学生1
克隆后的学生姓名:我是克隆对象更改后的学生2
*/

switch 语句

在 switch(expr)中,expr 只能是一个整数表达式或者枚举常量。而整数表达式可以是 int 基本数据类型或者是 Integer 包装类型,由于 byte、short、char 都可以隐式转换为 int,而 long 和 String 类型都不符合 switch 的语法规定,并且不能被隐式地转换为 int 类型,所以它们都不能作用于 switch 语句中。JDK1.7 版本后 switch 就可以作用在 String 上了

short s1 = 1; s1 = s1+1;

  1. 对于 short s1 = 1; s1 = s1+1; 来说,在 s1+1 运算时会自动提升表达式的类型为 int,那么将 int 型值赋给 short 型变量,s1 会出现类型转换错误,应改为:s1 =(short)(s1+1)
  2. 对于 short s1 = 1; s1+= 1; 来说,+= 是 Java 语言规定的运算符,Java 编译器会对它进行特殊处理,因此可以正确编译

注意:(x+= i)不等于(x = x+i)

  1. 第一个表达式使用的是复合赋值操作符,复合赋值表达式自动地将所执行计算的结果转型为其左侧变量的类型,
  2. 如果结果的类型与该变量的类型相同,那么这个转型不会造成任何影响。
  3. 如果结果的类型比该变量的类型要宽,那么复合赋值操作符将悄悄地执行一个窄化原生类型转换

final、finally 和 finalize 的区别

  1. final 用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖、类不可继承
  2. finally 作为异常处理的一部分,只能在 try/catch 语句中使用,finally 附带一个语句块用来表示这个语句最终一定被执行,经常被用在需要释放资源的情况下
  3. finalize 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的 finalize()方法。当垃圾收集器准备好释放对象占用空间时,首先会调用 finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存

BigDecimal

为什么会出现 4.0-3.6 = 0.4000001 这种现象?

2 进制的小数无法精确的表示 10 进制小数,计算机在计算 10 进制小数的过程中要先转换为 2 进制进行计算,这个过程中出现了误差

1
2
3
4
5
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false

使用 BigDecimal 解决精度丢失

1
2
3
4
5
6
7
8
9
10
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
1
2
3
4
5
6
7
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.add(b));// 1.9
System.out.println(a.subtract(b));// 0.1
System.out.println(a.multiply(b));// 0.90
System.out.println(a.divide(b));// 无法除尽,抛出 ArithmeticException 异常
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));// 1.11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum RoundingMode {
// 2.5 -> 3 , 1.6 -> 2
// -1.6 -> -2 , -2.5 -> -3
UP(BigDecimal.ROUND_UP),
// 2.5 -> 2 , 1.6 -> 1
// -1.6 -> -1 , -2.5 -> -2
DOWN(BigDecimal.ROUND_DOWN),
// 2.5 -> 3 , 1.6 -> 2
// -1.6 -> -1 , -2.5 -> -2
CEILING(BigDecimal.ROUND_CEILING),
// 2.5 -> 2 , 1.6 -> 1
// -1.6 -> -2 , -2.5 -> -3
FLOOR(BigDecimal.ROUND_FLOOR),
// 2.5 -> 3 , 1.6 -> 2
// -1.6 -> -2 , -2.5 -> -3
HALF_UP(BigDecimal.ROUND_HALF_UP),
//......
}