Java基础常见面试题

2020/1/1

# 1. 面向过程(POP) 与 面向对象(OOP)的区别

面向对象: Object Oriented Programming 面向过程: Procedure Oriented Programming

二者都是一种思想,面向对象是相对于面向过程而言的。

面向过程, 强调的是功能行为,以函数为最小单位,考虑怎么做

面向对象,将功能封装进对象, 强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。面向对象更加强调运用人类在日常的思维逻辑中采用的思想方法与原则,如抽象、分类、继承、聚合、多态等。

==错误言论:面向过程的性能比面向对象高?==因为类调⽤时需要实例化,开销⽐᫾⼤,⽐᫾消 耗资源,所以当性能是最重要的考量因素的时候,⽐如单⽚机、嵌⼊式开发、Linux/Unix 等 ⼀般采⽤⾯向过程开发。

这个并不是根本原因,⾯向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原 因并不是因为它是⾯向对象语⾔,⽽是 Java 是半编译语⾔,最终的执⾏代码并不是可以直 接被 CPU 执⾏的⼆进制机械码。 ⽽⾯向过程语⾔⼤多都是直接编译成机械码在电脑上执⾏,并且其它⼀些⾯向过程的脚本语 ⾔性能也并不⼀定⽐ Java 好

面向过程 :面向过程性能比面向对象高?? (opens new window)

# 2. Java 语⾔有哪些特点?

  1. 简单易学;
  2. ⾯向对象(封装,继承,多态);
  3. 平台⽆关性( Java 虚拟机实现平台⽆关性);
  4. 可靠性;
  5. 安全性;
  6. ⽀持多线程( C++ 语⾔没有内置的多线程机制,因此必须调⽤操作系统的多线程功能来进 ⾏多线程程序设计,⽽ Java 语⾔却提供了多线程⽀持);
  7. ⽀持⽹络编程并且很⽅便( Java 语⾔诞⽣本身就是为简化⽹络编程设计的,因此 Java 语 ⾔不仅⽀持⽹络编程⽽且很⽅便);
  8. 编译与解释并存;

# 3. 关于 JVM、 JDK 和 JRE

# 3.1 JVM

Java 虚拟机(JVM)是运⾏ Java 字节码的虚拟机。JVM 有针对不同系统的特定实现 (Windows,Linux,macOS),⽬的是使⽤相同的字节码,它们都会给出相同的结果。

字节码 和不同系统的 JVM 实现是 Java 语⾔“⼀次编译,随处可以运⾏”的关键所在。

# 3.1.1 什么是字节码?采⽤字节码的好处是什么

JVM 可以理解的代码就叫做==字节码==(即扩展名为 .class 的⽂件),它不⾯ 向任何特定的处理器,只⾯向虚拟机。

采⽤字节码好处:

  1. 在⼀定程度上解决了传统解释型语⾔执⾏效率低的问题

  2. 保留了解释型语言可移植的特点(一次编译,处处运行)。由于字节码并不针对⼀种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

# 3.1.2 为什么Java的字节码比传统的解释型语言效率高?
# 3.1.3 Java程序从源代码到运行的一般步骤

image-20210603164729638

==.class文件->机器码==这一步,JVM 类加载器⾸先加载字节码⽂件, 然后通过解释器逐⾏解释执⾏,这种⽅式的执⾏速度会相对比较慢。

而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。

机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言

# 3.2 JDK 和 JRE

JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。

JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。

什么情况下使用JDK和JRE?

如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。

# 4. 为什么说 Java 语言“编译与解释并存”?

高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。

Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(*.class 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。

# 5.Oracle JDK 和 OpenJDK 的对比(了解)

  1. Oracle JDK 大概每 6 个月发一次主要版本,而 OpenJDK 版本大概每三个月发布一次。但这不是固定的,我觉得了解这个没啥用处。详情参见:https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence 。
  2. OpenJDK 是一个参考模型并且是==完全开源==的,而 Oracle JDK 是 OpenJDK 的一个实现,并不是完全开源的;
  3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择 Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题;
  4. 在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能
  5. Oracle JDK 不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;
  6. Oracle JDK 使用 BCL/OTN 协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。

# 6.Java 和 C++的区别?

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java 不提供指针来直接访问内存,程序内存更加安全
  • Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
  • Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
  • C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。

# 7.import java 和 javax 有什么区别?(了解)

刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。

所以,实际上 java 和 javax 没有区别。

# 8.数据类型

# 8.1 基本数据类型

基本数据类型只有8种,可按照如下分类 ①整数类型:long、int、short、byte ②浮点类型:float、double ③字符类型:char ④布尔类型:boolean

基本类型 大小(字节) 数据范围 默认值 封装类
byte 1 -128(- 2^7^) ~ 127(2^8^-1) (byte)0 Byte
short 2 -32768(- 2^15^) ~ 32767(2^15^-1) (short)0 Short
int 4 -2147483648(- 2^31^) ~ 2147483647(2^31^-1) 0 Integer
long 8 - 2^63^ ~ 2^63^-1 0L Long
float 4 0.0f Float
double 8 0.0d Double
boolean - true或false false Boolean
char 2 0~255 \u0000(null) Character

# 8.2 引用数据类型

引用数据类型非常多,大致包括:类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型。

简单来说,所有的非基本数据类型都是引用数据类型

# 8.3 数据类型的注意事项

  1. int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值是 null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个对象, 再任何引用使用前,必须为其指定一个对象,否则会报错。
  2. 基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须 通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另一个数组 时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也会修改。
  3. 虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何 供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机 中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素 占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字节。使用int的原 因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的是32/64位系统,而 是指CPU硬件层面),具有高效存取的特点。参看:Java中boolean类型占用多少个字节 (opens new window)
  4. 一切的引用数据类型都可以使用Objec进行接收

# 8.4 基本数据类型和引用数据类型的区别

(1)存储位置

基本数据类型的存储位置:

==java中的基本数据类型一定存储在栈中的,这句话是错的!==

①局部变量

如果是是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量的变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈(方法栈),当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就==局部变量只能在方法中有效的原因==。

②成员变量(全局变量)

在类中声明的变量是成员变量,==基本类型的变量其变量名及其值放在堆内存中的==(因为全局变量不会随着某个方法执行结束而销毁)。

引用数据类型的存储位置:

当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆类存中的。

引用类型数据并不是直接存储在栈中,Java JVM会在堆中给数据分配内存空间,堆存储数据。栈存储的是指向对应堆的地址。可以说是栈中的地址引用了堆中的数据

补充:Java中数据的存放有堆和栈之分  当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的局部变量都是放在栈内存中的

当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。

(2)传递方式

基本数据类型:在方法中定义的非全局基本数据类型变量,调用方法时作为参数是按数值传递的。

当值类型变量a赋值给值类型变量b之后,再去改变b的值那么a不会随着b的改变而改变。

引用数据类型:引用数据类型变量,调用方法时作为参数是按引用传递的,传递的是对象的引用地址。

当引用值类型变量a赋值给引用值类型变量b之后,再去改变a的值那么b会随着a的改变而改变

(3)垃圾回收机制的区别

基本数据类型:

局部变量随着方法的结束内存自然销毁了,全局变量不会与引用数据类型相同。

引用数据类型:

当方法结束的时候,这个对象可能被另一个引用类型所应用,不会销毁,只有当一个对象没有任何引用变量引用的时候,垃圾回收机制才会回收

# 8.5 自动装箱与拆箱

在Java SE5之前,如果要生成一个数值为10的Integer对象,必须这样进行:

Integer i = new Integer(10);
1

而在从Java SE5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,只需要这 样就可以了:

Integer i = 10;
1
# 装箱

装箱:包装类使得一个基本数据类型的数据变成了类。有了类的特点,可以调用类中的方法。

# 装箱原理(调用包装类Xxx的valueOf()/使用构造方法实例化)
int i = 500;
Integer t = Integer.valueOf(i);
1
2

首先判断i值是否在-128和127之间,如果在-128和127之间则直接从IntegerCache.cache缓存中获取指定数字的包装类;不存在则new出一个新的包装类。

IntegerCache内部实现了一个Integer的静态常量数组,在类加载的时候,执行static静态块进行初始化-128到127之间的Integer对象,存放到cache数组中。cache属于常量,存放在java的方法区中。

只有double和float的自动装箱代码没有使用缓存,每次都是new 新的对象,其它的6种基本类型都使用了缓存策略。使用缓存策略是因为,缓存的这些对象都是经常使用到的(如字符、-128至127之间的数字),防止每次自动装箱都创建一此对象的实例。

# 拆箱

拆箱:将包装类中内容变为基本数据类型

# 拆箱原理(调用包装类Xxx的xxxValue())
Integer t = new Integer(500);
int j = t.intValue(); // j = 500, intValue取出包装类中的数据
1
2

JDK1.5之后,支持自动装箱,自动拆箱。但类型必须匹配。即包装类与基本数据类型可以直接赋值,不必再调用构造器或者xxxValue()。

参看:详解Java 自动装箱与拆箱的实现原理 (opens new window)

面试题: 以下代码会输出什么?

public class Main {
	public static void main(String[] args) {
		Integer i1 = 100;
		Integer i2 = 100;
		Integer i3 = 200;
		Integer i4 = 200;
		System.out.println(i1==i2);//true
		System.out.println(i3==i4);//false
	}
}
1
2
3
4
5
6
7
8
9
10

为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此 时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:

public static Integer valueOf(int i) {
	if(i >= -128 && i <= IntegerCache.high)
		return IntegerCache.cache[i + 128];
	else
		return new Integer(i);
}
1
2
3
4
5
6

其中IntegerCache类的实现为:

private static class IntegerCache {
    static final int high;
    static final Integer cache[];

    static {
        final int low = - 128;
        // high value may be configured by property
        int h = 127;
        if (integerCacheHighPropValue != null) {
            // Use Long.decode here to avoid invoking methods that
            // require Integer's autoboxing cache to be initialized
            int i = Long.decode(integerCacheHighPropValue).intValue();
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - - low);
        }
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for (int k = 0; k < cache.length; k++) {
            cache[k] = new Integer(j++);
        }
    }

    private IntegerCache() {}
}
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

从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。 上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。

# 9.字符型常量和字符串常量的区别?

  1. 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
  2. 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
  3. 占内存大小 : 字符常量只占 2 个字节; 字符串常量占若干个字节 (注意: char 在 Java 中占两个字节)

字符封装类 Character 有一个成员常量 Character.SIZE 值为 16,单位是bits,该值除以 8(1byte=8bits)后就可以得到 2 个字节

# 10.标识符和关键字

# 10.1 标识符和关键字的区别是什么?

标识符的含义: 是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等等,都是标识符。

但是有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这种特殊的标识符就是关键字。因此,关键字是被赋予特殊含义的标识符

比如,在我们的日常生活中 ,“警察局”这个名字已经被赋予了特殊的含义,所以如果你开一家店,店的名字不能叫“警察局”,“警察局”就是我们日常生活中的关键字。

# 10.2 标识符命名规则

命名规则:(硬性要求)

标识符可以包含英文字母,0-9的数字,$以及_

标识符不能以数字开头

标识符不是关键字

命名规范:(非硬性要求)

类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。

变量名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。

方法名规范:同变量名。

# 10.3 Java 中有哪些常见的关键字?

访问控制 private protected public
类,方法和变量修饰符 abstract class extends final implements interface native
new static strictfp synchronized transient volatile
程序控制 break continue return do while if else
for instanceof switch case default
错误处理 try catch throw throws finally
包相关 import package
基本类型 boolean byte char double float int long
short null true false
变量引用 super this void
保留字 goto const

# 10.4 this

# 10.4.1 this是什么?

使用:

  • 它在方法内部使用,即这个方法所属对象的引用;
  • 它在构造器内部使用,表示该构造器正在初始化的对象。

this理解为:当前对象当前正在创建的对象

# 10.4.2 什么时候使用this关键字呢?
  1. 在任意方法或构造器内,如果使用当前类的成员变量或成员方法可以在其前面添加this,增强程序的阅读性。不过,通常我们都习惯省略this。==很情况下,不使用this关键字(省略)表现相同, 但是,使用此关键字可能会使代码更易读或易懂。==
  2. 当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加this来表明该变量是类的成员变量
  3. 使用this访问属性和方法时,如果在本类中未找到,会从父类中查找
  4. this可以作为一个类中构造器相互调用的特殊格式
# 10.4.3 this的注意事项
  1. 可以在类的构造器中使用"this(形参列表)"的方式,调用本类中重载的其他的构造器!
  2. 明确:构造器中不能通过"this(形参列表)"的方式调用自身构造器
  3. 如果一个类中声明了n个构造器,则最多有 n - 1个构造器中使用了"this(形参列表)"
  4. "this(形参列表)"必须声明在类的构造器的首行!
  5. 在类的一个构造器中,最多只能声明一个"this(形参列表)"

# 10.5 super

# 10.5.1 super的使用

在Java类中使用super来调用父类中的指定操作:

  1. 我们可以在子类的方法或构造器中。通过使用"super.属性"或"super.方法"的方式,显式的调用父类中声明的属性或方法。但是,通常情况下,我们习惯省略"super."
  2. 特殊情况:当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式的使用"super.属性"的方式,表明调用的是父类中声明的属性。
  3. 特殊情况:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用"super.方法"的方式,表明调用的是父类中被重写的方法。

super调用父类构造器:

  1. 我们可以在子类的构造器中显式的使用"super(形参列表)"的方式,调用父类中声明的指定的构造器

  2. 我们在类的构造器中,针对于"this(形参列表)"或"super(形参列表)"只能二选一,不能同时出现,必须声明在子类构造器的首行!

  3. 在构造器的首行,没有显式的声明"this(形参列表)"或"super(形参列表)",则默认调用的是父类中空参的构造器:super()

  4. 在类的多个构造器中,至少有一个类的构造器中使用了"super(形参列表)",调用父类中的构造器;如果子类构造器中既未显式调用父类或本类的构造器, 且父类中又没有无参的构造器, 则编译出错必须通过this(参数列表)或者super(参数列表)语 句指定调用本类或者父类中相应的构造器。 同时, 只能”二选一”, 且必须放在构造器的首行

编译出错示例

父类

public class Person { 
    private String name; 
    private int age;
    private Date birthDate;
    
    public Person(String name, int age, Date d) {
        this.name = name;
        this.age = age;
        this.birthDate = d;
    }
    
    public Person(String name, int age) {
    	this(name, age, null);
    }
    
    public Person(String name, Date d) {
    	this(name, 30, d);
    }
    
    public Person(String name) {
    	this(name, 30);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

子类编译出错,父类无空参构造器,子类没有显示的通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器

public class Student extends Person {
	private String school;
    
    public Student(String name, int age, String s) {
        super(name, age);
        school = s;
    }
    
    public Student(String name, String s) {
        super(name);
        school = s;
    }
    
    // 编译出错: no super(),系统将调用父类无参数的构造器。
    public Student(String s) {
    	school = s;
    }
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 10.5.2 使用 this 和 super 的注意事项
  • 在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。
  • this、super不能用在static方法中。
    • 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西
  • 使用this会首先在本类找,然后去父类找;使用super会直接去父类找。

# 10.6 final

# 10.6.1 final的使用

final可以用来修饰的结构:类、方法、变量

  • 对于⼀个 final 变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象
    • final修饰成员变量:可以考虑赋值的位置有:显式初始化、代码块中初始化、构造器中初始化(每个构造器都需要赋值,否则报错)
    • final修饰局部变量:尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值。
  • final 修饰的类不能被其他类所继承final类中的所有成员方法都会被隐式的指定为final方法
  • final 修饰的方法不可以被重写
    • 使⽤ final ⽅法的原因有两个。第⼀个原因是把⽅法锁定,以防任何继承类修改它的含义;第⼆个原因是效率。在早期的 Java 实现版本中,会将 final ⽅法转为内嵌调⽤。但是如果⽅法过于庞⼤,可能看不到内嵌调⽤带来的任何性能提升(现在的 Java 版本已经不需要使⽤ final ⽅法进⾏这些优化了)。类中所有的 private ⽅法都隐式地指定为 final
# 10.6.2 final在方法返回时是否可以进行赋值和运算

final定义的变量不可以再次赋值,但返回时进行运算,相当于是新的变量

public class Something {
    public int addOne(final int x) {
    	return ++x;//不可以
    // return x + 1;//可以
    }
}
1
2
3
4
5
6
# 10.6.3 常量的属性是否可以进行运算

虽然Other是常量,但Other的属性和方法并不是常量

public class Something {
    public static void main(String[] args) {
        Other o = new Other();
        new Something().addOne(o);
    }
    public void addOne(final Other o) {
        // o = new Other();
        o.i++;
    }
}

class Other {
	public int i;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 10.7 static

# 10.7.1 背景

当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会产生出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下, 某些特定的数据在内存空间里只有一份

例如:所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

# 10.7.2 static的使用
# 10.7.2.1 修饰成员变量和成员方法
  • 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。
  • static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。
# 10.7.2.2 静态代码块

静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次。

# 10.7.2.3 静态内部类(static修饰类的话只能修饰内部类)

静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。

没有这个引用就意味着:

  1. 它的创建是不需要依赖外围类的创建。
  2. 它不能使用任何外围类的非static成员变量和方法。
# 10.7.2.4 静态导包(用来导入类中的静态资源,1.5之后的新特性)

格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。

# 10.7.3 被static修饰后的成员的特点
  1. 随着类的加载而加载
  2. 优先于对象存在
  3. 修饰的成员,被所有对象所共享
  4. 访问权限允许时,可不创建对象,直接被类调用
# 10.7.4 是否要使用static
  1. 类属性作为该类各个对象之间共享的变量。 在设计类时,分析哪些属性不因对象的不同而改变,将这些属性设置为类属性。相应的方法设置为类方法。
  2. 如果方法与调用者无关,则这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。

开发中,如何确定一个属性是否要声明为static的?

  1. 属性是可以被多个对象所共享的,不会随着对象的不同而不同的。

  2. 类中的常量也常常声明为static

如果想让一个类的所有实例共享数据,就用类变量!

开发中,如何确定一个方法是否要声明为static的?

  1. 操作静态属性的方法,通常设置为static的

  2. 工具类中的方法,习惯上声明为static的。 比如:Math、Arrays、Collections

# 10.8 instanceof

# 10.8.1 如何使用?

instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:

boolean result = obj instanceof Class;
1

其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或 间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。

int i = 0;
System.out.println(i instanceof Integer);//编译不通过 i必须是引用类型,不能是基本类型
System.out.println(i instanceof Object);//编译不通过
Integer integer = new Integer(1);
System.out.println(integer instanceof Integer);//true
System.out.println(null instanceof Object);//false ,在 JavaSE规范 中对 instanceof 运算符的规定就是:如果 obj 为 null,那么将返回false。
1
2
3
4
5
6
# 10.8.2 使用情景

使用情境:为了避免在向下转型时出现ClassCastException的异常,我们在向下转型之前,先进行instanceof的判断,一旦返回true,就进行向下转型。如果返回false,不进行向下转型。

public class Person extends Object {}

public class Student extends Person {}

public class Graduate extends Person {}

public void method1(Person e) {
    if (e instanceof Person)
    	// 处理Person类及其子类对象
    if (e instanceof Student)
    	//处理Student类及其子类对象
    if (e instanceof Graduate)
    	//处理Graduate类及其子类对象
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 10.8.3 对象类型转换 (Casting )

基本数据类型的Casting:

  • 自动类型转换:小的数据类型可以自动转换成大的数据类型如long g=20; double d=12.0f
  • 强制类型转换: 可以把大的数据类型强制转换(casting)成小的数据类型,如 float f=(float)12.0; int a=(int)1200L

对Java对象的强制类型转换称为造型

  • 从子类到父类的类型转换可以自动进行
  • 从父类到子类的类型转换必须通过造型(强制类型转换)实现
  • 无继承关系的引用类型间的转换是非法的
  • 在造型前可以使用instanceof操作符测试一个对象的类型
public class Test {
    public void method(Person e) { // 设Person类中没有getschool() 方法
        // System.out.pritnln(e.getschool()); //非法,编译时错误
        if (e instanceof Student) {
            Student me = (Student) e; // 将e强制转换为Student类型
            System.out.pritnln(me.getschool());
    	}
    }
    public static void main(String[] args){
        Test t = new Test();
        Student m = new Student();
        t.method(m);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 11.键盘输入

方法 1:通过 Scanner

Scanner input = new Scanner(System.in);
String s  = input.nextLine();
input.close();
1
2
3

方法 2:通过 BufferedReader

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
1
2

# 12.重载和重写的区别

# 重载(Overload)

重载就是同⼀个类中多个同名⽅法根据不同的传参来执⾏不同的逻辑处理。

# 重写(Override)

重写发⽣在运⾏期,是⼦类对⽗类的允许访问的⽅法的实现过程进⾏重新编写。

  1. 返回值类型、⽅法名、参数列表必须相同,抛出的异常范围⼩于等于⽗类,访问修饰符范围⼤于等于⽗类。
  2. 如果⽗类⽅法访问修饰符为 private/final/static 则⼦类就不能重写该⽅法,但是被 static 修饰的⽅法能够被再次声明。
  3. 构造⽅法⽆法被重写

关于 重写的返回值类型:如果⽅法的返回类型是void和基本数据类型,则返回值重写时不可修改。但是如果⽅法的返回值是引⽤类型,重写时是可以返回该引⽤类型的⼦类的。

# 区别

image-20210603210448752

# 13.equals与==

# 13.1 ==

它的作⽤是判断两个对象的地址是不是相等。即,判断两个对象是不是同⼀个对象(基本数据类型==⽐较的是值,引⽤数据类型==⽐较的是内存地址)。

  1. 比较的是操作符两端的操作数是否是同一个对象。
  2. 两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。
  3. 比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为true,如:int a=10long b=10Ldouble c=10.0都是相同的(为true),因为他们都指向地址为10的堆

# 13.2 equals()

equals() : 它的作⽤也是判断两个对象是否相等。但它⼀般有两种使⽤情况:

情况 1:类没有覆盖 equals() ⽅法。则通过 equals() ⽐较该类的两个对象时,等价于通过“==”⽐较这两个对象。 情况 2:类覆盖了 equals() ⽅法。⼀般,我们都覆盖 equals() ⽅法来⽐较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

  1. equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断。
  2. 所有比较是否相等时,都是用equals 并且在对常量相比较时,把常量写在前面,因为使用object的equals object可能为null则空指针

# 13.3 对象相等与指向他们的引⽤相等,两者有什么不同?

对象的相等,⽐的是内存中存放的内容是否相等。⽽引⽤相等,⽐较的是他们指向的内存地址是否相等。

# 13.4 使用 == 比较枚举类型

由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用 == 运算符比较两个变量,如上例所示;此外,== 运算符可提供编译时和运行时的安全性。

首先,让我们看一下以下代码段中的运行时安全性,其中 == 运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException:

Pizza.PizzaStatus pizza = null;
System.out.println(pizza.equals(Pizza.PizzaStatus.DELIVERED));//空指针异常
System.out.println(pizza == Pizza.PizzaStatus.DELIVERED);//正常运行
1
2
3

对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较:

if (Pizza.PizzaStatus.DELIVERED.equals(TestColor.GREEN)); // 编译正常
if (Pizza.PizzaStatus.DELIVERED == TestColor.GREEN);      // 编译失败,类型不匹配
1
2

# 14.Java ⾯向对象编程三⼤特性: 封装、继承、多态

# 14.1 封装

封装把⼀个对象的属性私有化,同时提供⼀些可以被外界访问的属性的⽅法,如果属性不想被外界访问,我们⼤可不必提供⽅法给外界访问。但是如果⼀个类没有提供给外界访问的⽅法,那么这个类也没有什么意义了。

# 14.1.1 为什么需要封装?封装的作用和含义?

举例:我要用洗衣机,只需要按一下开关和洗涤模式就可以了。有必要了解洗衣机内部的结构吗?有必要碰电动机吗?

使用者对类内部定义的属性(对象的成员变量)的直接操作会导致数据的错误、混乱或安全性问题。所以应该使用封装,将信息隐藏.

# 14.1.2 我们程序设计追求“高内聚,低耦合”。

高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;

低耦合 : 仅对外暴露少量的方法用于使用。

# 14.1.3封装性的设计思想

隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性。通俗的说, 把该隐藏的隐藏起来,该暴露的暴露出来。 这就是封装性的设计思想。

# 14.1.4 四种访问权限修饰符

Java权限修饰符public、 protected、 (缺省)、 private置于类的成员定义前,用来限定对象对该类成员的访问权限。

  1. Java规定的4种权限(从小到大排列):private、缺省、protected 、public

  2. 4种权限可以用来修饰类及类的内部结构:属性、方法、构造器、内部类

  3. 具体的,4种权限都可以用来修饰类的内部结构:属性、方法、构造器、内部类

  4. 修饰类的话,只能使用:缺省、public

修饰符 类内部 同一个包 不同包的子类 同一个工程
private yes
default(缺省) yes yes
protected yes yes yes
public yes yes yes yes
# 注意:

对于class的权限修饰只可以用public和default(缺省)。

  • public类可以在任意地方被访问。
  • default类只可以被同一个包内部的类访问。

# 14.2 继承

继承是使⽤已存在的类的定义作为基础建⽴新类的技术,新类的定义可以增加新的数据或新的功能,也可以⽤⽗类的功能,但不能选择性地继承⽗类。通过使⽤继承我们能够⾮常⽅便地复⽤以前的代码。

# 14.2.1 关于继承如下 3 点请记住:
  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。(以后介绍)。
# 14.2.2 作用
  1. 继承的出现减少了代码冗余,提高了代码的复用性。
  2. 继承的出现,更有利于功能的扩展。
  3. 继承的出现让类与类之间产生了关系,提供了多态的前提。
# 14.2.3 子类对象实例化的全过程
  1. 从结果上来看:(继承性)
  • 子类继承父类以后,就获取了父类中声明的属性或方法。

  • 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。

  1. 从过程上来看:
  • 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,....

  • 直到调用了java.lang.Object类中空参的构造器为止。

  • 正因为加载过所有的父类的结构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。

注意:

虽然创建子类对象时,调用了父类的构造器,但是自始至终就创建过一个对象,即为new的子类对象。

# 14.2.4 继承成员变量和继承方法的区别
  • 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
  • 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量
# 14.2.5 阻止继承

正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。

# 14.2.6 继承的规则

  1. 一个类可以被多个子类继承。

  2. Java中类的单继承性:一个类只能有一个父类

  3. 子父类是相对的概念。

  4. 子类直接继承的父类,称为:直接父类。间接继承的父类称为:间接父类

  5. 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法

  6. 子类不能直接访问父类中私有的(private)的成员变量和方法。

  7. Java只支持单继承和多层继承, 不允许多重继承

    • 一个子类只能有一个父类

    • 一个父类可以派生出多个子类

      class SubDemo extends Demo{ } //ok
      class SubDemo extends Demo1,Demo2...//error
      
      1
      2

# 14.2.7 子类对象实例化的全过程

  1. 从结果上来看:(继承性)
  • 子类继承父类以后,就获取了父类中声明的属性或方法。

  • 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。

  1. 从过程上来看:
  • 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,....

  • 直到调用了java.lang.Object类中空参的构造器为止。

  • 正因为加载过所有的父类的结构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。

# 注意:

虽然创建子类对象时,调用了父类的构造器,但是自始至终就创建过一个对象,即为new的子类对象。

# 14.2.8 为什么super(…)和this(…)调用语句不能同时在一个构造器中出现?只能作为构造器中的第一句出现?

# 14.3 多态

多态,顾名思义,表示一个对象具有多种的状态。具体表现为==父类的引用指向子类的实例==。即编译时类型与运行时类型不一致。

针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。

# 14.3.1 多态性的使用

虚拟方法调用

有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法。

总结:编译,看左边;运行,看右边。

# 14.3.2 多态性的注意事项

Java引用变量有两个类型: 编译时类型和运行时类型。 编译时类型由声明该变量时使用的类型决定, 运行时类型由实际赋给该变量的对象决定。 简称: 编译时, 看左边;运行时, 看右边。

  • 若编译时类型和运行时类型不一致, 就出现了对象的多态性(Polymorphism)

  • 多态情况下, “看左边” : 看的是父类的引用(父类中不具备子类特有的方法);“看右边” : 看的是子类的对象(实际运行的是子类重写父类的方法)

  • 对象的多态 —在Java中,子类的对象可以替代父类的对象使用

    • 一个变量只能有一种确定的数据类型

    • 一个引用类型变量可能指向(引用)多种不同类型的对象

      Person p = new Student();Object o = new Person();//Object类型的变量o, 指向Person类型的对象o = new Student(); //Object类型的变量o, 指向Student类型的对象
      
      1
  • 子类可看做是特殊的父类, 所以父类类型的引用可以指向子类的对象:向上转型(upcasting)。

  • 一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法

    Student m = new Student();m.school = “pku”; //合法,Student类有school成员变量Person e = new Student();e.school = “pku”; //非法,Person类没有school成员变量
    
    1

    属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。

  • 对象的多态性,只适用于方法,不适用于属性(编译和运行都看左边)

# 14.3.3 多态性的使用前提

① 类的继承或者实现关系 ② 方法的重写 ③向上转型

# 14.3.4虚拟方法调用(Virtual Method Invocation)

正常的方法调用

Person e = new Person();e.getInfo();Student e = new Student();e.getInfo();
1

虚拟方法调用(多态情况下)

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

Person e = new Student();e.getInfo(); //调用Student类的getInfo()方法
1

编译时类型和运行时类型

编译时e为Person类型,而方法的调用是在运行时确定的,所以调用的是Student类的getInfo()方法。 ——动态绑定

# 14.3.5 方法的重载与重写与多态性(方法的重载是多态性的一种体现?NO)*

从编译和运行的角度看: 重载,是指允许存在多个同名方法,而这些方法的参数不同。 编译器根据方法不同的参数表, 对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。 它们的调用地址在编译期就绑定了。 Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法

所以: 对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定” ;

对于多态,只有等到方法调用的那一刻, 解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”

引用一句Bruce Eckel的话: “不要犯傻,如果它不是晚绑定, 它就不是多态。

所以说,方法的重载并不是多态性的一种体现

# 15. String、StringBuffer和StringBuilder

# 15.1 String详解

# 15.1.1 String不可变为什么还可以再次赋值?

 String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。 String对象的字符内容是存储在一个字符数组value[]中的。 这里有个问题,既然String对象是字符串常量不可改变,那么我们经常对其进行多次赋值,那是怎么回事的呢?下面的例子是我们不能理解的

String s = "aaa";
s = "bbb";
// 打印出来的s为bbb
System.out.println(s);
1
2
3
4

原来,上面的对String进行重新赋值的操作会将值bbb赋值给生成新的String对象,而旧的对象值aaa则是等待回收。

String s = "ccc";
s = s +"ddd";
// 打印出来的s为cccddd
System.out.println(s)
1
2
3
4

上面的String字符串拼接,看似改变了原有的值,其实只是假象。真正的原理是: ①String.valueOf(str1) ②产生StringBuilder, 调用的StringBuilder(str1)构造方法, 把StringBuilder初始化,长度为str1.length()+16,并且调用append(str1) ③调用StringBuilder.append(str2), 把第二个字符串拼接进去, 然后调用StringBuilder.toString返回结果

//Java1.5前为StringBuffer,1.5之后为StringBuilder
StringBuilder.append("ccc").append("ddd").toString();
1
2

  String的字符串拼接是要额外占据内存的,以“hello”拼接“world”为例,短短的两个字符串,却需要开辟三次内存空间(极大浪费): image-20200726215027215

# 15.1.2 String初始化情况

image-20200726215050975

image-20200726215116524

# 15.1.3String str1 = “abc”String str2 = new String(“abc”)的区别?

字符串常量存储在字符串常量池, 目的是共享。 字符串非常量对象存储在堆中。

image-20200726215200345

常量与常量的拼接结果在常量池。 且常量池中不会存在相同内容的常量。

只要其中有一个是变量, 结果就在堆中

如果拼接的结果调用intern()方法, 返回值就在常量池

注意事项:

  1. String s1 = "a"; 说明:在字符串常量池中创建了一个字面量为"a"的字符串。
  2. s1 = s1 + "b"; 说明:实际上原来的“a”字符串对象已经丢弃了, 现在在堆空间中产生了一个字符串s1+"b"(也就是"ab")。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能。
  3. String s2 = "ab"; 说明:直接在字符串常量池中创建一个字面量为"ab"的字符串。
  4. String s3 = "a" + "b"; 说明: s3指向字符串常量池中已经创建的"ab"的字符串。
  5. String s4 = s1.intern(); 说明:堆空间的s1对象在调用intern()之后,会将常量池中已经存在的"ab"字符串赋值给s4。

# 15.2 StringBuffer与StringBuilder详解

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象

StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高

StringBuffer b = new StringBuffer("123");
b.append("456");
// b打印结果为:123456
System.out.println(b);
1
2
3
4

b对象的内存空间图:

image-20200726215214142

没有产生新的字符串,所以说StringBuffer对象是一个字符序列可变的字符串,它没有重新生成一个对象,而且在原来的对象中可以连接新的字符串。

# 15.3 String、StringBuffer和StringBuilder的共同之处

三者继承关系:

image-20200726215227735

三者共同之处:

都是final类,不允许被继承,主要是从性能和安全性上考虑的,因为这几个类都是经常被使用着,且考虑到防止其中的参数被参数修改影响到其他的应用;底层都使用char[]存储

# 15.4 String、StringBuffer和StringBuilder的区别之处

可变性

简单的来说:String 类中使⽤ final 关键字修饰字符数组来保存字符串, private final char value[] ,所以 ==String 对象是不可变的==。

StringBuilderStringBuffer 都继承⾃ AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使⽤字符数组保存字符串 char[] value 但是没有⽤ final 关键字修饰,所以这两种对象都是可变的。

线程安全性

String 中的对象是不可变的,也就可以理解为常量,线程安全。

StringBuffer 对⽅法加了同步锁或者对调⽤的⽅法加了同步锁,所以是线程安全的。

StringBuilder 并没有对⽅法进⾏加同步锁,所以是⾮线程安全的。

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。

StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。

相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

运行速度(执行速度):StringBuilder > StringBuffer > String

实现接口

String实现了三个接口:SerializableComparable<String>CarSequence;StringBuilder只实现了两个接口SerializableCharSequence,相比之下String的实例可以通过compareTo方法进行比较,其他两个不可以.

# 15.4 使用情况

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

# 16.深拷贝与浅拷贝

浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

# 17.接口和抽象类

# 17.1 抽象类

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

抽象类是用来模型化那些父类无法确定全部实现,而是由其子类提供具体实现的对象的类。

abstract关键字来修饰一个类, 这个类叫做抽象类。

# 17.1.1 抽象类的使用
  • abstract可以用来修饰的结构:类、方法
  • 不能用abstract修饰变量、代码块、构造器;
  • 不能用abstract修饰私有方法、静态方法、 final的方法、 final的类。
# 17.1.2抽象类的特点
  • 抽象类不能被实例化。抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。

  • 抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)

  • 开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作

# 17.2 抽象方法

用abstract来修饰一个方法, 该方法叫做抽象方法。只有方法的声明,没有方法的实现以分号结束。

抽象方法的特点

  1. 抽象方法只有方法的声明,没有方法体

  2. 包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法的。

  3. 若子类重写了父类中的所有的抽象方法后,此子类方可实例化

  4. 若子类没有重写父类中的所有的抽象方法,则此子类也是一个抽象类,需要使用abstract修饰

# 17.3 接口

接口(interface)是抽象方法和常量值定义的集合。

Java中,接口和类是并列的两个结构

# 17.3.1 为什么使用接口?
  1. 一方面, 有时必须从几个类中派生出一个子类, 继承它们所有的属性和方法。 但是, Java不支持多重继承。 有了接口, 就可以得到多重继承的效果。
  2. 另一方面, 有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有is-a的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打印机、扫描仪、摄像头、充电器、 MP3机、手机、数码相机、移动硬盘等都支持USB连接。
# 17.3.2接口的特点
  1. 用interface来定义。
  2. 接口中的所有成员变量都默认是由public static final修饰的。
  3. 接口中的所有抽象方法都默认是由public abstract修饰的。
  4. 接口中没有构造器!意味着接口不可以实例化
  5. 接口采用多继承机制。
  6. 实现接口的类中必须提供接口中所有方法的具体实现内容,方可实例化。否则,仍为抽象类。
  7. 与继承关系类似,接口与实现类之间存在多态性
  8. 接口和类是并列关系, 或者可以理解为一种特殊的类。 从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义(JDK7.0及之前), 而没有变量和方法的实现(JDK8后拥有)。
# 17.3.3如何定义接口?

JDK7以前:

只能定义全局常量和抽象方法

全局常量:public static final的.但是书写时,可以省略不写

抽象方法:public abstract的

JDK8:

除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法(略)

Jdk 9:

在接⼝中引⼊了私有⽅法和私有静态⽅法

# 17.3.4 JDK8关于接口的改进

Java 8中,你可以为接口添加静态方法默认方法。从技术角度来说,这是完全合法的,只是它看起来违反了接口作为一个抽象定义的理念。

# 17.3.4.1 静态方法

使用 static 关键字修饰。 可以通过接口直接调用静态方法,并执行其方法体。我们经常在相互一起使用的类中使用静态方法。你可以在标准库中找到像Collection/Collections或者Path/Paths这样成对的接口和类。

interface Flyable{
	
	//全局常量
	public static final int MAX_SPEED = 7900;//第一宇宙速度
	int MIN_SPEED = 1;//省略了public static final
	
	//抽象方法
	public abstract void fly();
	//省略了public abstract
	void stop();

}
1
2
3
4
5
6
7
8
9
10
11
12
# 17.3.4.2默认方法

默认方法使用 default 关键字修饰。可以通过实现类对象来调用。我们在已有的接口中提供新方法的同时,还保持了与旧版本代码的兼容性。比如: java 8 API中对Collection、 List、 Comparator等接口提供了丰富的默认方法。

interface Filial {// 孝顺的
    default void help() {
    	System.out.println("老妈,我来救你了");
    }
}

interface Spoony {// 痴情的
    default void help() {
    	System.out.println("媳妇,别怕,我来了");
    }
}

class Man implements Filial, Spoony {
    @Override
    public void help() {
    	System.out.println("我该怎么办呢?");
    	Filial.super.help();
    	Spoony.super.help();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

注意事项

  1. 如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,报错。会出现: 接口冲突。 解决办法:实现类必须覆盖接口中同名同参数的方法,来解决冲突(在实现类中重写此方法)。
  2. 若一个接口中定义了一个默认方法,而父类中也定义了一个同名同参数的非抽象方法,则不会出现冲突问题。因为此时遵守: 类优先原则。 接口中具有相同名称和参数的默认方法会被忽略。(如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法。)
  3. 接口中定义的静态方法,只能通过接口来调用。
  4. 通过实现类的对象,可以调用接口中的默认方法。如果实现类重写了接口中的默认方法,调用时,仍然调用的是重写以后的方法。

# 17.4 接口VS抽象类

  1. 接口的⽅法默认是 public ,所有⽅法在接口中不能有实现(Java 8 开始接口⽅法可以有默认实现),⽽抽象类可以有⾮抽象的⽅法。
  2. 接口中除了 static 、 final 变量,不能有其他变量,⽽抽象类中则不⼀定。
  3. ⼀个类可以实现多个接口,但只能实现⼀个抽象类。接口⾃⼰本身可以通过 extends 关键字扩展多个接口。
  4. 接口⽅法默认修饰符是 public ,抽象⽅法可以有 public 、 protected 和 default 这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!)。
  5. 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,⽽接口是对⾏为的抽象,是⼀种⾏为的规范。
image-20200604182003689

在开发中,常看到一个类不是去继承一个已经实现好的类,而是要么继承抽象类,要么实现接口。

# 18.成员变量与局部变量的区别有哪些?

  1. 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  2. 从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  3. 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  4. 从变量是否有默认值来看,成员变量如果没有被赋初,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

# 19.hashCode()与 equals()

面试官可能会问你:“你重写过 hashcodeequals么,为什么重写 equals 时必须重写 hashCode 方法?”

# 19.1 hashCode()介绍

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。

public native int hashCode();Copy to clipboardErrorCopied
1

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

# 19.2 为什么要有 hashCode?

为什么说hashcode可以提高检索效率呢?我们先看一个例子,如果想判断一个集合是否包含某个对象,最简单的做法是怎样的呢?逐一取出集合中的每个元素与要查找的对象进行比较,当发现该元素与要查找的对象进行equals()比较的结果为true时,则停止继续查找并返回true,否则,返回false。如果一个集合中有很多个元素,比如有一万个元素,并且没有包含要查找的对象时,则意味着你的程序需要从集合中取出一万个元素进行逐一比较才能得到结论,这样做的效率是非常低的。

这时,可以采用哈希算法(散列算法)来提高从集合中查找元素的效率,将数据按特定算法直接分配到不同区域上。将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组(使用不同的hash函数来计算的),每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储在哪个区域,大大减少查询匹配元素的数量。

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head First Java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

# 19.3 为什么重写 equals 时必须重写 hashCode 方法?

如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。

hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

假设我们我们重写了对象的equals(),但是不重写hashCode()方法,由于超类Object中的hashcode()方法始终返回的是一个对象的内存地址,而不同对象的这个内存地址永远是不相等的。这时候,即使我们重写了equals()方法,也不会有特定的效果的,因为不能确保两个equals()结果为true的两个对象会被散列在同一个存储区域,即 obj1.equals(obj2) 的结果为true,但是不能保证 obj1.hashCode() == obj2.hashCode() 表达式的结果也为true;这种情况,就会导致数据出现不唯一,因为如果连hashCode()都不相等的话,就不会调用equals方法进行比较了,所以重写equals()就没有意义了。

# 19.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?

因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode

刚刚提到的 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。

更多关于 hashcode()equals() 的内容可以查看:

# 19.5 哈希Code产生冲突时的解决办法

有可能在产生hash冲突时,两个不相等的对象就会有相同的 hashcode 值,当hash冲突产生时,一般有以下几种方式来处理:

  • 拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储.
  • 开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
  • 再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突.

# 19.6 equals()与hashCode()的联系

Java的超类Object类已经定义了equals()和hashCode()方法,在Obeject类中,equals()比较的是两个对象的内存地址是否相等,而hashCode()返回的是对象的内存地址。所以==hashCode主要是用于查找使用的,而equals()是用于比较两个对象是否相等的==。但有时候我们根据特定的需求,可能要重写这两个方法,在重写这两个方法的时候,主要注意保持一下几个特性:

(1)如果两个对象的equals()结果为true,那么这两个对象的hashCode一定相同;

(2)两个对象的hashCode()结果相同,并不能代表两个对象的equals()一定为true,只能够说明这两个对象在一个散列存储结构中。

(3)如果对象的equals()被重写,那么对象的hashCode()也要重写。

# 19.7 基本数据类型和String类型的hashCode()方法和equals()方法

(1)hashCode():八种基本类型的hashCode()很简单就是直接返回他们的数值大小,String对象是通过一个复杂的计算方式,但是这种计算方式能够保证,如果这个字符串的值相等的话,他们的hashCode就是相等的。

(2)equals():8种基本类型的equals方法就是直接比较数值,String类型的equals方法是比较字符串的值的。

# 20.值传递

# 20.1 为什么 Java 中只有值传递?

首先,我们回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。

按值调用(call by value) 表示方法接收的是调用者提供的值,按引用调用(call by reference) 表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。

Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。

下面通过 3 个例子来给大家说明

example 1

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);}
1

结果:

a = 20b = 10num1 = 10num2 = 20
1

解析:

image-20210604090855439

在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。

通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.

example 2

    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;    }
1

结果:

10
1

解析:

image-20210604090921641

array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。

通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。

很多程序设计语言(特别是,C++和 Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。

example 3

public class Test {    public static void main(String[] args) {        // TODO Auto-generated method stub        Student s1 = new Student("小张");        Student s2 = new Student("小李");        Test.swap(s1, s2);        System.out.println("s1:" + s1.getName());        System.out.println("s2:" + s2.getName());    }    public static void swap(Student x, Student y) {        Student temp = x;        x = y;        y = temp;        System.out.println("x:" + x.getName());        System.out.println("y:" + y.getName());    }}
1

结果:

x:小李y:小张s1:小张s2:小李
1

解析:

交换之前:

image-20210604090943772

交换之后:

image-20210604091001835

通过上面两张图可以很清晰的看出: 方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝

总结

Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按 值传递的。

下面再总结一下 Java 中方法参数的使用情况:

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

# 20.2 方法参数的值传递机制

形参:方法定义时,声明的小括号内的参数

实参: 方法调用时,实际传递给形参的数据

# 20.2.1Java的实参值如何传入方法呢?

Java里方法的参数传递方式只有一种: 值传递。 即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。

  • 形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参
  • 形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参
# 20.2.2基本数据类型的参数传递

形参不会修改堆空间实参的具体值,因为形参此时存储的是实参的数据值

public static void main(String[] args) {
    int x = 5;
    System.out.println("修改之前x = " + x);// 5
    // x是实参
    change(x);
    System.out.println("修改之后x = " + x);// 5
}

public static void change(int x) {
    System.out.println("change:修改之前x = " + x);//5
    x = 3;
    System.out.println("change:修改之后x = " + x);//3
}
1
2
3
4
5
6
7
8
9
10
11
12
13
image-20200530105739133
# 20.2.3 引用数据类型的参数传递

形参会修改堆空间实参的具体值,因为形参此时存储的是实参的地址引用.

public static void main(String[] args) {
    Person obj = new Person();
    obj.age = 5;
    System.out.println("修改之前age = " + obj.age);// 5
    // x是实参
    change(obj);
    System.out.println("修改之后age = " + obj.age);// 3
}

public static void change(Person obj) {
    System.out.println("change:修改之前age = " + obj.age);//5
    obj.age = 3;
    System.out.println("change:修改之后age = " + obj.age);//3
}

//其中Person类定义为:
class Person{
	int age;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 22.异常处理

# 22.1 异常分类

Throwable

所有异常的父类(都继承java.lang.Throwable)

  • Error Java应用程序语法恢复的严重异常,不需要捕获和处理;发生时,一般通知用户并终止程序执行(如: JVM系统内部错误、 资源耗尽等严重情况。 比如: StackOverflowError)

  • Exception

    Java应用程序抛出和处理的非严重错误,称为异常。是所有Java异常的父类。(因编程错误或偶然的外在因素导致的一般性问题, 可以使用针对性的代码进行处理)

    • RuntimeExceotion 运行时异常,编程时不处理,也可以编译通过(例如,数组下标越界)

    • CheckedException 非运行时异常,必须编译时处理,否则不通过(例如,类找不到)

# 22.2 Throwable 类常用方法

  • public string getMessage():返回异常发生时的简要描述
  • public string toString():返回异常发生时的详细信息
  • public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
  • public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息。用于输出异常的堆栈信息包括程序运行到当前类的执行流程,显示方法调用序列(获取异常类名和异常信息,以及异常出现在程序中的位置。)

# 22.3 try-catch-finally

  • try块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块: 用于处理 try 捕获到的异常。
  • finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

在以下 3 种特殊情况下,finally 块不会被执行:

  1. tryfinally块中用了 System.exit(int)退出程序。但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执行
  2. 程序所在的线程死亡。
  3. 关闭 CPU。

# 22.4 使用 try-with-resources 来代替try-catch-finally

  1. 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象
  2. 关闭资源和 finally 块的执行顺序:try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

《Effecitve Java》中明确指出:

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点。

Java 中类似于InputStreamOutputStreamScannerPrintWriter等的资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求,如下:

        //读取文本文件的内容        Scanner scanner = null;        try {            scanner = new Scanner(new File("D://read.txt"));            while (scanner.hasNext()) {                System.out.println(scanner.nextLine());            }        } catch (FileNotFoundException e) {            e.printStackTrace();        } finally {            if (scanner != null) {                scanner.close();            }        }Copy to clipboardErrorCopied
1

使用 Java 7 之后的 try-with-resources 语句改造上面的代码:

try (Scanner scanner = new Scanner(new File("test.txt"))) {    while (scanner.hasNext()) {        System.out.println(scanner.nextLine());    }} catch (FileNotFoundException fnfe) {    fnfe.printStackTrace();}Copy to clipboardErrorCopied
1

当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单,如果你还是用try-catch-finally可能会带来很多问题。

通过使用分号分隔,可以在try-with-resources块中声明多个资源。

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));             BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {            int b;            while ((b = bin.read()) != -1) {                bout.write(b);            }        }        catch (IOException e) {            e.printStackTrace();        }
1

# 22.5 常见异常

  • NullPointerException空指针异常 属于运行时异常,调用了未经初始化的对象或不存在的对象,或是访问或修改null对象的属性或方法(例如,数组初始化和数组元素初始化混淆)

  • ClassNotFoundException类没能找到的异常 原因①的确不存在该类 ②环境进行了调整(目录结构发生变化,编译、运行路径发生变化)​ ③修改类名时没有修改调用该类的其他类​

  • IllegalArgumentException表明向方法传递了一个不合法或不正确的参数

  • InputMismatchException由Scanner抛出 表明Scanner获取内容与期望类型的模式不匹配,或该内容超出期望类型范围

  • IllegalAccessException 应用程序试图创建一个实例、设置或获取一个属性,或者调用一个方法,但当前正在执行的方法无法访问指定类、属性、方法或构造方法定义时,抛出

  • ClassCastException试图将对象强制转换为不是实例的子类时抛出异常

  • SQLException提供关于数据库访问错误或其他信息的异常

  • IOException是失败或中断的I/O操作生成的异常的通用类

# 23.序列化和反序列化

序列化:将java对象转化为字节序列的过程。

反序列化:将字节序列转化为java对象的过程。

# 23.1 Java 序列化中如果有些字段不想进⾏序列化,怎么办?

对于不想进⾏序列化的变量,使⽤ transient 关键字修饰。

transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法。

# 23.2 什么时候需要序列化

  • 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
  • 当你想用套接字在网络上传送对象的时候;
  • 当你想通过RMI传输对象的时候;

一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

# 23.3 如何序列化?

java.io.ObjectOutputStream表示对象输出流,它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

java.io.ObjectInputStream表示对象输入流,它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回。

只有实现了SerializableExternalizable接口的类的对象才能被序列化,否则抛出异常。

序列化:

​ 步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(“目标地址路径”));
1

​ 步骤二:通过对象输出流的writeObject()方法写对象:

out.writeObject("Hello");out.writeObject(new Date());
1

反序列化:

​ 步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:

ObjectInputStream in = new ObjectInputStream(new fileInputStream(“目标地址路径”));
1

​ 步骤二:通过对象输出流的readObject()方法读取对象:

String obj1 = (String)in.readObject();
Date obj2 =  (Date)in.readObject();
1
2

​ 说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。

# 23.4 序列化ID(serialVersionUID)的作用

这个序列化ID起着关键的作用,它决定着是否能够成功反序列化!简单来说,java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。

为了不必要的报错麻烦: 序列化时最好是定义序列化版本id 即 public static final Long seriaVersionUID = 1L (默认) 或者 xxxxx L(自定义64位都行)

因为反序列化会判断序列化中的id和类中的id是否一样,如果不定义虽然会自动生成,但如果后面改了东西列,所以还是自觉点定义一个id,省去好多麻烦

同时记住静态变量不会被序列化的,它可不在堆内存中,序列化只会序列化堆内存

# 24.Comparable 和 Comparator 的区别

comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序

comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

# 24.1 Comparable的使用

//如果要比较大小,必须实现Comparable接口
class Employee implements Comparable<Employee> {

    private int id;
    private String name;
    private int salary;

    @Override
    public int compareTo(Employee o) {  //告诉sort()方法比较规则
        return o.salary - this.salary;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo03Comparable {

    public static void main(String[] args) {

        Employee[] employees = {
                new Employee(1, "孙悟空",7000),
                new Employee(2, "孙悟天",5000),
                new Employee(3, "孙悟饭",6000),
        };

       System.out.println("排序前:");
        for (Employee employee : employees) {
            System.out.println(employee);
        }

        //排序
        Arrays.sort(employees);

        System.out.println("排序后:");
        for (Employee employee : employees) {
            System.out.println(employee);
        }
    }

}
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

# 24.2 Comparator的使用

class Student {
    private String name;
    private int age;
    private int score;
}

//年龄比较器
class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge() - o2.getAge();
    }
}
//成绩比较器
class ScoreComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getScore() - o2.getScore();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo04Comparator {
    public static void main(String[] args) {
        //2)    创建多个学生对象添加到集合中
        List<Student> students = new ArrayList<>();
        students.add(new Student("张三",20,86));
        students.add(new Student("李四",18,95));
        students.add(new Student("老王",24,67));

        System.out.println("排序前:");
        System.out.println(students);

        //3)    创建类Demo04Comparator按年龄从小到大对集合的学生对象进行排序
        Collections.sort(students, new AgeComparator());
        System.out.println("年龄排序后:");
        System.out.println(students);
        
        //按分数进行排序
        Collections.sort(students, new ScoreComparator());
        System.out.println("分数排序后:");
        System.out.println(students);
	}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 24.3 区别

Comparable & Comparator 都是用来实现集合中的排序的,只是 Comparable 是在对象内部定义的方法实现的排序,Comparator 是在集合外部实现的排序。

  1. Comparable位于包java.lang下,Comparator位于包java.util下。 Comparable接口将比较代码嵌入自身类中,而Comparable在一个独立的类中实现比较。

  2. 如果类的设计师没有考虑到Compare的问题而没有实现Comparable接口, 可以通过Comparator 来实现比较算法进行排序。

  3. Comparator为了使用不同的排序规则做准备。比如:升序、降序或按不同的属性进行排序。

# 25.BigDecimal

# 25.1 BigDecimal 的用处

《阿里巴巴Java开发手册》中提到:浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。 具体原理和浮点数的编码方式有关,这里就不多提了,我们下面直接上实例:

float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false
1
2
3
4
5

具有基本数学知识的我们很清楚的知道输出并不是我们想要的结果(精度丢失),我们如何解决这个问题呢?一种很常用的方法是:使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。

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
8
9
10

# 25.2 BigDecimal 的大小比较

a.compareTo(b) : 返回 -1 表示 a 小于 b,0 表示 a 等于 b , 1表示 a 大于 b

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.compareTo(b));// 1
1
2
3

# 25.3 BigDecimal 保留几位小数

通过 setScale方法设置保留几位小数以及保留规则。保留规则有挺多种,不需要记,IDEA会提示。

BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN);
System.out.println(n);// 1.255
1
2
3

# 25.4 BigDecimal 的使用注意事项

注意:我们在使用BigDecimal时,为了防止精度丢失,推荐使用它的 BigDecimal(String) 构造方法来创建对象。《阿里巴巴Java开发手册》对这部分内容也有提到如下图所示。

image-20210606154600110

# 26. Arrays.sort 原理

在数组的数量小于47的情况下使用插入排序,在大于或等于47或少于286会进入快速排序(双轴快排)大于286采用归并排序

插入排序:

我们知道插入排序不用去遍历整个数组,整个排序算法的核心性能消耗即是移位,当数组过大之后,会导致进行大量的移位操作,但是当数组较小的时候,插入排序的效果反倒会更好。