Java基础
参考:JavaGuide,www.javalearn.cn, www.pdai.tech等
一、基础概念与常识
1. Java和C++对比
-
指针。 Java 没有指针的概念。在 C/C++中,指针操作内存时,经常会出现错误。 Java没有指针更安全
-
多重继承 。C++支持多继承,而Java不支持多重继承,但允许一个类实现多个接口
-
数据类型。
- Java 是完全面向对象的语言,所有方法和数据都必须是类的一部分。除了基本数据类型之外,其余类型的数据都作为对象型数据。eg: 对象型数据包括字符串和数组。类将数据和方法结合起来,把它们封装在其中,这样每个对象都可实现具有自己特点的行为。此外,Java 还取消了 C/C++中的结构和联合,使编译程序更加简洁。
- 而 C++将函数和变量定义为全局的,然后再来调用这些函数和变量。
-
自动内存管理 。 Java 自动进行无用内存回收操作,不再需要程序员进行手动删除,而 C++ 中必须由程序释放内存资源
-
操作符重载。 Java 不支持操作符重载,操作符重载则被认为是 C++ 的突出特征 (不过 Java语言还是可以通过类来实现操作符重载所具有的功能的 )
-
预处理功能 。
- C/C++在编译过程中都有一个预编译阶段,即预处理器。预处理器为开发人员提供了方便,但增加了编译的复杂性。
- Java 允许预处理,但不支持预处理器功能,因为 Java 没有预处理器,所以为了实现预处理,它提供了引入语句(import),但它与 C++预处理器的功能类似。
-
缺省参数函数 。 Java 不支持缺省参数函数,而 C++支持 。( 所谓缺省参数,顾名思义,就是在声明函数的某个参数的时候为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就无须指定该参数。)
-
字符串 。
- C 和 C++不支持字符串变量,在 C 和 C++程序中使用“Null”终止符代表字符串的结束。
- 在 Java 中字符串是用类对象(String 和 StringBuffer)来实现的。Java 字符串类是作为 Java 语言的一部分定义的,而不是作为外加的延伸部分。
-
goto 语句。 goto 语句是 C 和 C++ 的“遗物”,Java 不提供 goto 语句,虽然 Java 指定 goto 作为关键字,但不支持它的使用,这使程序更简洁易读
-
类型转换 。
- 在 C 和 C++中,有时会出现数据类型的隐含转换,这就涉及了自动强制类型转换问题。例如,在 C++中可将一个浮点值赋予整型变量,并去掉其尾数。
- Java 不支持 C++中的自动强制类型转换,如果需要,必须由程序显式进行强制类型转换。
2. Java语言有哪些特点
-
面向对象(封装,继承,多态)。 Java 语言提供类、接口和继承等面向对象的特性
-
平台无关性。Java的虚拟机机制,使Java“一次编写,随处运行(Write once, Run anywhere)”,在不同平台上运行不需要重新编译
-
支持多线程。C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持;
-
支持网络编程并且很方便。Java 语言诞生本身就是为简化网络编程设计的
3. 什么是字节码?采用字节码的好处?
-
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为
.class
的文件),它不面向任何特定的处理器,只面向虚拟机。 -
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
4. JVM,JRE和JDK
-
JDK是(Java Development Kit)的缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
-
JRE是Java Runtime Environment缩写,它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
-
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
-
JDK包含JRE,JRE包含JVM。
5. Oracle JDK和OpenJDK区别
-
Oracle JDK 版本将每三年发布一次,而 OpenJDK 版本每三个月发布一次;
-
OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是OpenJDK 的一个实现,并不是完全开源的;
例: native方法,Oracle的JDK是看不到的,OpenJDK或其他开源JRE是可以找到对应的C/C++代码
-
Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,建议选择 Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题;
-
在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能;
-
Oracle JDK 不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;
-
Oracle JDK 根据二进制代码许可协议获得许可,而 OpenJDK 根据 GPLv2 许可获得许可。
- 既然 Oracle JDK 这么好,那为什么还要有 OpenJDK?
- OpenJDK是开源的,可以自己对他优化修改;
- OpenJDK 是商业免费的。虽然 Oracle JDK 也是商业免费(eg: JDK 8),但并不是所有版本都是免费的。
- OpenJDK 更新频率更快。
二、基本语法
1. 关键字
分类 | 关键字 | ||||||
---|---|---|---|---|---|---|---|
访问控制 | private | protected | public | ||||
类,方法和变量修饰符 | abstract | class | extends | final | implements | interface | native |
new | static | strictfp | synchronized | transient | volatile | enum | |
程序控制 | break | continue | return | do | while | if | else |
for | instanceof | switch | case | default | assert | ||
错误处理 | try | catch | throw | throws | finally | ||
包相关 | import | package | |||||
基本类型 | boolean | byte | char | double | float | int | long |
short | |||||||
变量引用 | super | this | void | ||||
保留字 | goto | const |
1) final
浅析Java中的final关键字 - Matrix海子 - 博客园 (cnblogs.com) 绝!
上面这篇文章问到一个问题为什么匿名内部类可以访问的外部成员必须是final修饰的,拓展理解Java四种内部类
Java内部类详解 - Matrix海子 - 博客园 (cnblogs.com)
2) static
Static静态代码块以及各代码块之间的执行顺序 - 掘金 (juejin.cn)
3) this 和 super
Java 中 this 和 super 的用法总结 | 菜鸟教程 (runoob.com)
3) instanceof
Java关键字(一)——instanceof - YSOcean - 博客园 (cnblogs.com)
4) native
Java关键字(二)——native - YSOcean - 博客园 (cnblogs.com)
5) volatile
Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)_老鼠只爱大米的博客-优快云博客_java的volatile
6) synchronized
Java中synchronized关键字作用及用法_江湖人称小程的博客-优快云博客
2. 变量
1) 成员变量与局部变量的区别?
-
语法形式 :从语法形式看,成员变量属于类,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰;但是,成员变量和局部变量都能被final
所修饰。 -
存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。(堆栈都放什么???) -
生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
-
默认值 :从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
2) 静态变量有什么作用?
-
静态变量可以被类的所有实例共享。无论一个类创建了多少个对象,它们都共享同一份静态变量。
-
通常情况下,静态变量会被
final
关键字修饰成为常量
3) 字符型常量和字符串常量的区别?
- 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
- 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小 : 字符常量只占 2 个字节; 字符串常量占若干个字节。(注意:
char
在 Java 中占两个字节
3. 方法
1) 静态方法为什么不能调用非静态成员?
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
2) 静态方法和实例方法有何不同?
- 调用方式。 调用静态方法时,可以使用
类名.方法名
的方式,也可以使用对象.方法名
的方式 。但实例方法只能用对象.方法名
的方式。( 注意的是一般不建议使用对象.方法名
的方式来调用静态方法。 ) - 访问类成员是否存在限制。 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制
3) 重载和重写区别
-
重载就是同一个类中多个同名方法,根据不同的传参来执行不同的逻辑处理。
-
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法 ,( 重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。 ) 重写发生在运行期
-
方法名和参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类 (重写要遵循“两同两小一大” )
(关于 重写的返回值类型 这里需要额外多说明一下:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。 )
-
如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 -
构造方法无法被重写
区别点 重载方法 重写方法 发生范围 同一个类 子类 参数列表 必须修改 一定不能修改 返回类型 可修改 子类方法返回值类型应比父类方法返回值类型更小或相等 异常 可修改 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; 访问修饰符 可修改 一定不能做更严格的限制(可以降低限制) 发生阶段 编译期 运行期
-
4) 什么是可变长参数?
public static void method1(String arg1, String... args) {
//......
}
- 从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。
- 可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。
- 遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢? 会优先匹配固定参数的方法
- Java 的可变参数编译后实际会被转换成一个数组,我们看编译后生成的
class
文件就可以看出来了
5) 构造方法
1. 类没有声明构造方法能正确执行吗?
- 可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
- 如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了。我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。
- 如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。
- ⚠️构造器不能被继承,因此不能被重写override,但可以被重载。每一个类必须有自己的构造函数,负责构造自己这部分的构造。子类不会覆盖父类的构造函数,相反必须一开始调用父类的构造函数。
2. 构造方法特点
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
6) 值传递和引用传递的区别的什么?为什么说Java中只有值传递?
-
值传递:指的是在方法调用时,传递的参数是值的拷贝,也就是说传递后就互不相关了。
-
引用传递:指的是在方法调用时,传递的参数是按引用进行传递,传递的是引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(即同一个内存空间)。
-
基本类型作为参数被传递时肯定是值传递;引用类型作为参数被传递时也是值传递,“值”为对应的引用。
4. 基本数据类型
1) Java的几种基本数据类型(8种)
-
6 种数字类型:
- 4 种整数型:
byte
、short
、int
、long
- 2 种浮点型:
float
、double
- 4 种整数型:
-
1 种字符类型:
char
-
1 种布尔型:
boolean
基本类型 位数 字节 默认值 取值范围 byte
8 1 0 -128 ~ 127 short
16 2 0 -32768 ~ 32767 int
32 4 0 -2147483648 ~ 2147483647 long
64 8 0L -9223372036854775808 ~ 9223372036854775807 char
16 2 ‘u0000’ 0 ~ 65535 float
32 4 0f 1.4E-45 ~ 3.4028235E38 double
64 8 0d 4.9E-324 ~ 1.7976931348623157E308 boolean
1 false true、false 这八种基本类型都有对应的包装类分别为:
Byte
、Short
、Integer
、Long
、Character
、Float
、Double
、Boolean
-
引用数据类型建立在基本数据类型的基础上,包括数组、类和接口。
⚠️ 注:Java 语言中不支持 C++中的指针类型、结构类型、联合类型和枚举类型。
2) 基本类型和包装类型的区别
-
成员变量包装类型不赋值就是
null
,而基本类型有默认值且不是null
。 -
包装类型可用于泛型,而基本类型不可以
-
存放位置。基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被
static
修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,而几乎所有对象实例都存在于堆中。 -
相比于对象类型, 基本数据类型占用的空间非常小。
⚠️ 注:什么时候该用包装类,什么时候用基本类型,看基本的业务来定:这个字段允不允许null值,如果允许null值,则必然要用封装类,否则值类型就可以了,用到比如泛型和反射调用函数.,就需要用包装类!
3) 自动装箱与拆箱
-
装箱:将基本类型用它们对应的引用类型包装起来;
-
拆箱:将包装类型转换为基本数据类型;
Integer i = 10; //装箱 int n = i; //拆箱
-
原理:看上面两行代码的对应的.class文件可以发现, 装箱其实就是调用了 包装类的
valueOf()
方法,拆箱其实就是调用了xxxValue()
方法。 即Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
⚠️注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作
4) 包装类型的缓存机制了解吗?
-
前提:发生在自动装箱的过程(基本类型–>包装类型)
- 创建一个包装类对象有两种方法:
(1)构造器方法(就是new出来);
(2)自动装箱(就是编译器自动调用包装类的valueOf方法); - 两种方法的区别:
构造器方法:不论值的大小,返回的将都会是一个新对象;
自动装箱会先经过判断,再决定返回的是一个新对象还是常量池中已存在的对象。
- 创建一个包装类对象有两种方法:
-
机制:当通过自动装箱机制创建包装类对象时,首先会判断数值是否在-128—-127的范围内,如果满足条件,则会从缓存(常量池)中寻找指定数值,若找到缓存,则不会新建对象,只是指向指定数值对应的包装类对象,否则,新建对象。
-
原理:(看Integer缓存的源码)
- 当包装类Integer加载时,该包装类中的内部类IntegerCache会初始化一个包装类类型数组,最小值(固定值)为-128,而最大值(默认值)为127【可修改】,这个长度的缓存值放在方法区的常量池中,是所有线程共享的。
- 当发生自动包装的时候,调用valueOf方法,对需要包装的基本类型的值进行判断,如果在缓存值的范围内,则返回缓存的对象,否则创建一个新的对象返回。看自动装箱的valueOf方法源码(Integer类型例)
-
作用:
- 在缓存值范围内的对象可以直接在常量池中取出,不用创建新的对象;
- 当需要频繁的使用同一对象的时候,如果有缓存,则可以避免重复创建同一对象,节省空间开销和时间消耗,提升了性能。
-
⚠️注意:
-
不是所有的包装类都有缓存机制。( Float,Double,Boolean 三大包装类并没有缓冲机制 )
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回True
orFalse
。 -
所有整型包装类对象之间值的比较,全部使用equals方法比较。
(因为:在[-128,127] 区间内的会复用已有的对象,而区间外的都会在堆上产生不会复用已有的对象)
-
5) 浮点数运算可能会丢失精度
1. 为什么
-
数据在内存(计算机)中是以二进制的形式存在的
-
十进制小数转换成二进制时,有可能会取不尽。( 十进制数的二进制表示形式可能不精确。 )
比如:0.9(10)= 0.1110011001100…(2),其二进制表示是无限不循环的
-
而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断。
所以,当浮点数在内存中进行运算时,很大概率上会发生精度丢失。例:
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
⚠️注:double类型精度丢失原因与float类型其实是一样的,区别在于有效位数。
2. 如何解决浮点数运算的精度丢失问题
-
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。通常大部分需要浮点数精确运算结果的业务场景(eg涉及到钱的场景)都是通过BigDecimal
来做的。实际开发中不建议直接使用float和double进行运算 -
在使用
BigDecimal
时,为了防止精度丢失,推荐使用它的BigDecimal(String val)
构造方法或者BigDecimal.valueOf(double val)
静态方法来创建对象。
6) 超过 long 整型的数据应该如何表示
-
BigInteger
内部使用int[]
数组来存储任意大小的整形数据。BigInteger
运算的效率会相对较低。//Java 中,64 位 long 整型是最大的整数类型。 long l = Long.MAX_VALUE; System.out.println(l + 1); // -9223372036854775808 System.out.println(l + 1 == Long.MIN_VALUE); // true
-
BigDecimal和BigInteger都在java.math包
三、面向对象基础
1. 面向对象和面向过程的区别
-
面向过程是 是一种以过程为中心的编程思想,直接将解决问题的步骤分析出来,然后用函数把步骤一步一步实现,然后再依次调用;
而面向对象是将构成问题的事物, 抽象出对象,然后用对象执行方法的方式解决问题。
-
面向过程编程,数据和对数据的操作是分离的。 面向对象编程,数据和对数据的操作是绑定在一起的。
2. 面向对象三大特性
-
封装 : 指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
-
继承: 它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。
⚠️
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。 (重写)
-
多态: 它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
简单的说:就是用基类的引用指向子类的对象。
⚠️
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
3. 什么是多态及如何实现多态
-
什么是多态机制:一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须由程序运行期间才能决定,这就是多态性
多态性可以分为编译时多态和运行时多态。- 编译时多态:主要是指方法的重载,它是根据参数列表的不同来区分不同的函数
- 运行时多态: 即通常所说的多态,运行时多态是动态的,它是通过动态绑定来实现
-
多态如何实现
Java实现多态有三个必要条件:继承、重写(覆盖)、向上转型- 继承:在多态中必须存在有继承关系的子类和父类
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备调用父类的方法和子类的方法(当子类中重写了父类的方法时,则调用时会调用子类的方法)
4. 对象实体与对象引用有何不同?
-
new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
-
一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
一个对象实例可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
-
对象的相等一般比较的是内存中存放的内容是否相等。引用相等一般比较的是他们指向的内存地址是否相等
5. 接口和抽象类的区别
语法层面上的区别:
- 方法:抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
- 成员变量:抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
- 静态有无:接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
- 继承关系:一个类只能继承一个抽象类,而一个类却可以实现多个接口。
设计层面上的区别:
-
抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。
抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
-
设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
6. Java创建对象的方式
-
new创建新对象
-
通过反射机制
-
采用clone机制: 对于clone机制,需要注意浅拷贝和深拷贝的区别。 clone方法可能会抛出异常,需要处理。
-
通过序列化机制: 把已经创建好的类持久化到本地,然后再读取,这个过程也属于常见类的方式,注意异常处理,别忘了类实现Serializable接口
//例 public class Student implements Serializable,Cloneable{ public Student(){ super(); } @Override protected Student clone() throws CloneNotSupportedException { return (Student) super.clone(); } } //1.new创建新对象 Student student = new Student(); //2.反射创建:newInstance()方法调用默认构造器(无参数构造器)初始化新建对象。 Student student = Student.class.newInstance(); //3.clone创建 Student student = new Student(); Student clone = student.clone(); //4.序列化机制创建 Student student = (Student) new ObjectInputStream(new FileInputStream("file.txt")).readObject();
7. 深拷贝和浅拷贝区别
-
引用拷贝: 创建一个指向对象的引用变量的拷贝。 即两个不同的引用指向同一个对象
-
对象拷贝: 创建对象本身的一个副本。 深拷贝和浅拷贝都是对象拷贝
-
浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。
简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
-
深拷贝:是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
-
四、Java常见类
1. Object类
-
Java Object 类是所有类的父类,即 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。
-
Object类源码的常见方法
private static native void registerNatives(); static { registerNatives(); } /** * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。 */ public final native Class<?> getClass() /** * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。 */ public native int hashCode() /** * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。 */ public boolean equals(Object obj) /** * native 方法,用于创建并返回当前对象的一份拷贝。 */ protected native Object clone() throws CloneNotSupportedException /** * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。 */ public String toString() /** * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 */ public final native void notify() /** * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。 */ public final native void notifyAll() /** * native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。 */ public final native void wait(long timeout) throws InterruptedException /** * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。 */ public final void wait(long timeout, int nanos) throws InterruptedException /** * 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念 */ public final void wait() throws InterruptedException /** * 实例被垃圾回收器回收的时候触发的操作 */ protected void finalize() throws Throwable { }
1)registerNatives方法
- registerNatives本质就是一个本地方法,当该类被加载的时候,调用该方法完成对该类中本地方法的注册。
- 在Object类中,除了有registerNatives这个本地方法之外,还有hashCode()、clone()等本地方法。也就是说,**凡是包含registerNatives()本地方法的类,同时也包含了其他本地方法。**所以,显然,当包含registerNatives()方法的类被加载的时候,要注册的方法就是该类所包含的除了registerNatives()方法以外的所有本地方法。 (底层C++实现)
2)== 和 equals() 的区别
-
==
对于基本类型和引用类型的作用效果是不同的:-
对于基本数据类型来说,
==
比较的是值。 -
对于引用数据类型来说,
==
比较的是对象的内存地址。⚠️因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
-
-
equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()
方法存在于Object
类中,因此所有的类都有equals()
方法。-
equals() 方法的两种使用情况
- 类没有重写
equals()
方法 :通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object
类equals()
方法。 - 类重写了
equals()
方法 :一般我们都重写equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
- 类没有重写
-
String
中的equals
方法是被重写过的。Object
的equals
方法是比较的对象的内存地址,而String
的equals
方法比较的是对象的值。String a = new String("ab"); // a 为一个引用 String b = new String("ab"); // b为另一个引用,对象的内容一样 String aa = "ab"; // 放在常量池中 String bb = "ab"; // 从常量池中查找 System.out.println(aa == bb);// true System.out.println(a == b);// false System.out.println(a.equals(b));// true System.out.println(42 == 42.0);// true //当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
-
3)hashCode() ?
1. hashCode()作用
-
hashCode()的作用:获取哈希码 ,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。 hashCode返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现。
-
从Object角度看,JVM每new一个Object,它都会将这个Object丢到一个Hash表中去,这样的话,下次做Object的比较或者取这个对象的时候(读取过程),它会根据对象的HashCode再从Hash表中取这个对象。
-
hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
另外需要注意的是: Object 的 hashCode()方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
-
理解:Java中的集合有两类,一类是List,再有一类是Set。前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。 equals方法可用于保证元素不重复,但如果每增加一个元素就检查一次,若集合中现在已经有1000个元素,那么第1001个元素加入集合时,就要调用1000次equals方法。这显然会大大降低效率。
所以:
2. 为什么要有 hashCode?
-
当你把对象加入
HashSet
时,HashSet
会先计算对象的hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode
值作比较。 -
如果没有相符的
hashCode
,HashSet
会假设对象没有重复出现。 -
但是如果发现有相同
hashCode
值的对象,这时会调用equals()
方法来检查hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。 -
这样我们就大大减少了
equals
的次数,相应就大大提高了执行速度。hashCode
大大缩小了查找成本。
3. hashCode特点
-
如果两个对象的**
hashCode
值相等,那这两个对象不一定相等**(哈希碰撞)。 -
如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。 -
如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。⚠️因为
hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的hashCode
)。
4)为什么重写 equals() 时必须重写 hashCode() 方法?
-
因为两个相等的对象的
hashCode
值必须是相等。也就是说如果equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。如果重写
equals()
时没有重写hashCode()
方法的话就可能会导致equals
方法判断是相等的两个对象,hashCode
值却不相等。 -
重写
equals()
时没有重写hashCode()
方法的话,会出现什么问题?-
重写了equals方法,不重写hashCode方法时,可能会出现equals方法返回为true,而hashCode方法却返回不同的结果。
-
影响: 在java底层的集合框架中(如HashMap,HashSet等),为了提高查询的效率,在确定某个对象的存储位置时,往往需要通过首先调用对象的hashCode方法来实现。 若涉及到判断两个对象是否相等时,重写了equals()方法,二者的equals()后判断对象相同,但是java底层在实现时会先调用hashCode方法,因为没有重写,返回是不一样的,就会造成判断对象不同,判断错误。
-
2. String类
1)String、StringBuffer、StringBuilder 的区别?
-
可变性。
-
String是不可变的。
private final char value[];
-
StringBuilder 与 StringBuffer都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用
final
和private
关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如append
方法。所以,StringBuilder 与 StringBuffer是可变的
-
-
线程安全性
-
String
中的对象是不可变的,也就可以理解为常量,线程安全。 -
AbstractStringBuilder
是StringBuilder
与StringBuffer
的公共父类,定义了一些字符串的基本操作,如expandCapacity
、append
、insert
、indexOf
等公共方法。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。(爸爸安全)StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
-
-
性能
- 每次对
String
类型进行改变的时候,都会生成一个新的String
对象,然后将指针指向新的String
对象。 StringBuffer
每次都会对StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。- 相同情况下使用
StringBuilder
相比使用StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
- 每次对
-
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
- 操作少量的数据: 适用
2)String 为什么是不可变的?
-
看String源码分析
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; …
-
在定义字符串时,会将字符串内容保存到一个使用 private final修饰的char[ ]之中,从源码中我们可以得知该数组被final修饰,因此它的引用地址不能改变。
-
但是这并不代表char value[ ]数组中的内容不可变,我们依旧可以通过数组下标来修改value数组,
-
String不可变的主要原因是
- 存放数据的char[ ]数组被private修饰,我们从外部无法去访问到char[ ],并且String内部本身也没有向我们提供修改char value[ ]的API, 从而无法对字符串内容进行修改。
- 而且String类被final修饰,导致其不能被继承,避免了子类破坏String的不可变。
-
-
从缓存池角度看——String缓存池
-
String Pool 是在方法区的一块特殊存储区域。当一个String被创建时如果发现当前String已经存在于String Pool,则会返回一个已存在String的引用而不会新建一个对象。
-
以下代码只会创建一个String对象在堆内存中。
String s1 = "hello"; String s2 = "hello";
-
在缓存池中,如果一个String是可变的,改变了一个引用指向的String,那么就会导致其他引用得到错误的值。因此String不可被改变。
-
3)字符串常量池
-
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
-
当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
// 在堆中创建字符串对象”ab“ // 将字符串对象”ab“的引用保存在字符串常量池中 String aa = "ab"; // 直接返回字符串常量池中字符串对象”ab“的引用 String bb = "ab"; System.out.println(aa==bb);// true
4)String str="aaa"与 String str=new String(“aaa”)一样吗?
- 使用
String a = “aaa” ;
,程序运行时会在常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。 - 使用String b = new String(“aaa”);`,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池,相当于创建了两个对象,无论常量池中有没有”aaa”字符串,程序都会在堆内存中开辟一片新空间存放新对象。
5) intern() 方法
String.intern() 是一个 native方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,分为两种情况:
-
如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
-
如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
// 在堆中创建字符串对象”Java“ // 将字符串对象”Java“的引用保存在字符串常量池中 String s1 = "Java"; // 直接返回字符串常量池中字符串对象”Java“对应的引用 String s2 = s1.intern(); // 会在堆中在单独创建一个字符串对象 String s3 = new String("Java"); // 直接返回字符串常量池中字符串对象”Java“对应的引用 String s4 = s3.intern(); // s1 和 s2 指向的是堆中的同一个对象 System.out.println(s1 == s2); // true // s3 和 s4 指向的是堆中不同的对象 System.out.println(s3 == s4); // false // s1 和 s4 指向的是堆中的同一个对象 System.out.println(s1 == s4); //true
6)字符串拼接用“+” 还是 StringBuilder?
- Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
- 字符串对象通过“+”的字符串拼接方式,看对应的.class文件,实际上是通过
StringBuilder
调用append()
方法实现的,拼接完成之后调用toString()
得到一个String
对象 。 - 不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个
StringBuilder
以复用,会导致创建过多的StringBuilder
对象。 如果直接使用StringBuilder
对象进行字符串拼接的话,就不会存在这个问题了。
7)String 类型的变量, 常量分别做“+”运算时发生什么?
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing"; //常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string"; //常量池中的对象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
-
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
-
有一个编译器的优化技术——常量折叠。 常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
eg: 对于 String s1 = “1” + “2”; 编译器会给你优化成 String s1 = “12”;
-
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型(
byte
、boolean
、short
、char
、int
、float
、long
、double
)以及字符串常量。 final
修饰的基本数据类型和字符串变量- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
- 基本数据类型(
-
引用的值在程序编译期是无法确定的, 在运行时才能知道其确切值, 编译器无法对其进行优化。
对象引用和“+”的字符串拼接方式,实际上是通过
StringBuilder
调用append()
方法实现的,拼接完成之后调用toString()
得到一个String
对象 。 -
不过,字符串使用
final
关键字声明之后,可以让编译器当做常量来处理。 编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。final String str1 = "str"; final String str2 = "ing"; // 下面两个表达式其实是等价的 String c = "str" + "ing";// 常量池中的对象 String d = str1 + str2; // 常量池中的对象 System.out.println(c == d);// true
-------------------------------------------------
五、异常机制
Java异常类层次结构图

1. Exception 和 Error 的区别
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
-
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。 -
Error
:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获 。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复。
2. Checked Exception 和 Unchecked Exception 区别?
-
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被
catch
或者throws
关键字处理的话,就没办法通过编译。常见的受检查异常有: IO 相关的异常、
ClassNotFoundException
、SQLException
…。 -
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException
及其子类都统称为非受检查异常,常见的有:NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户)- …
-
非受检查异常和受检查异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检查异常,否则就选择非受检查异常。
3. throw声明异常 和 throws抛出异常
-
throws 声明异常: 若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法头中使用关键字throws,后面接上要声明的异常。
public static void method() throws IOException, FileNotFoundException{ …… }
-
注意:若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。
-
如果是不可查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。
-
必须声明方法可抛出的任何受检查异常(checked exception)。即如果一个方法可能出现受可查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误。
-
通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。
仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
-
-
throw 抛出异常: 如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它。
public static double method(int value) { if(value == 0) { throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常 } return 5.0 / value; }
- 大部分情况下都不需要手动抛出异常,因为Java的大部分方法要么已经处理异常,要么已声明异常。所以一般都是捕获异常或者再往上抛。
- 有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。
4. 捕获异常
异常捕获处理的方法通常有:
- try-catch
- try-catch-finally
- try-finally
- try-with-resource
1)try-catch-finally 如何使用?
-
try
块 : 用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。 -
catch
块 : 用于处理 try 捕获到的异常。 -
finally
块 : 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在
try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。 -
⚠️ 注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
-
执行的顺序
- 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
- 当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
- 当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;
-
finally 中的代码一定会执行吗?
不一定。在某些情况下,finally 中的代码不会被执行。
finally 中的代码不会被执行的情况:
-
程序所在的线程死亡
-
关闭 CPU
-
finally 之前虚拟机被终止运行
try { System.out.println("Try to do something"); throw new RuntimeException("RuntimeException"); } catch (Exception e) { System.out.println("Catch Exception -> " + e.getMessage()); // 终止当前正在运行的Java虚拟机 System.exit(1); } finally { System.out.println("Finally"); }
-
2)如何用 try-with-resources 代替try-catch-finally?
-
面对必须要关闭的资源,我们总是应该优先使用
try-with-resources
而不是try-finally
。 -
try-with-resources是jdk1.7加入的机制,可以保证资源使用后正常关闭,并使代码更加简洁。
//try-catch-finally使用实例: public void handle(String fileName) { BufferedReader reader = null; try { String line; reader = new BufferedReader(new FileReader(fileName)); while ((line = reader.readLine()) != null) { ... } } catch (Exception e) { ... } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { ... } } } } //finally 中的 close 方法也可能抛出 IOException, 需要处理 //可以看到为了保证resources正常关闭,finally中又使用if语句以及try-catch,增加了代码的复杂性。 //使用try-with-resources: public void handle(String fileName) { try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) { String line; while ((line = reader.readLine()) != null) { ... } } catch (Exception e) { ... } } //try-with-resources明显节省了很多代码,资源在try后边的()中生成,在try结束后程序会自动关闭资源。
-
try-with-resources明显节省了很多代码,资源在try后边的()中生成,在try结束后程序会自动关闭资源。
-
⚠️ 注意,try后边括号中声明的资源必须实现java.lang.AutoCloseable, 如果声明没实现这个接口的变量,IDE会进行提示’The resource type File does not implement java.lang.AutoCloseable’。
5. 异常小结
- throws:声明一个异常,告知方法调用者。
- throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关。
- 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
六、泛型机制
1. 什么是泛型
- 泛型是 JDK1.5 的一个新特性,泛型就是将类型参数化,其在编译时才确定具体的参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
- **泛型只存在于编译阶段,而不存在于运行阶段。**在编译后的 class 文件中,是没有泛型这个概念的
2. 泛型的使用
1)泛型类
class Point<T>{ // 此处可以随便写标识符号,T是type的简称
private T var ; // var的类型由T指定,即:由外部指定
public T getVar(){ // 返回值的类型由外部决定
return var ;
}
public void setVar(T var){ // 设置的类型也由外部决定
this.var = var ;
}
}
Point<String> p = new Point<String>() ; // 里面的var类型为String类型
//多元泛型
class Notepad<K,V>{ // 此处指定了两个泛型类型
private K key ; // 此变量的类型由外部决定
private V value ; // 此变量的类型由外部决定
……
}
2)泛型接口
interface Info<T>{ // 在接口上定义泛型
public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型
}
class InfoImpl<T> implements Info<T>{ // 定义泛型接口的子类
private T var ; // 定义属性
public InfoImpl(T var){ // 通过构造方法设置属性内容
this.setVar(var) ;
}
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
}
//实例化
Info<String> i = null; // 声明接口对象
i = new InfoImpl<String>("汤姆") ; // 通过子类实例化对象
3)泛型方法
-
定义泛型方法语法格式
-
说明一下,定义泛型方法时,必须在返回值前边加一个< T>,来声明这是一个泛型方法,持有一个泛型T
修饰符 <T,E,…> 返回值类型 方法名(形参列表){ …… } //形参列表通常要有一个与T相关的参数,这样才能在调用的时候指定T
- 调用泛型方法的语法
3. 泛型的限定通配符和非限定通配符 ?
-
限定通配符包括两种:
- 表示类型的上界,格式为:<? extends T>,即类型必须为T类型或者T子类
- 表示类型的下界,格式为:<? super T>,即类型必须为T类型或者T的父类
-
非限定通配符:类型为,可以用任意类型来替代。
七、反射机制
1. 什么是反射
-
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java的反射机制。Java反射机制在框架设计中极为广泛。
but, 使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
2. 反射的应用场景
-
平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
-
注解 的实现也用到了反射。
为什么你使用 Spring 的时候 ,一个
@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个@Value
注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。
-
jdbc就是典型的反射
Class.forName('com.mysql.jdbc.Driver.class');//加载MySQL的驱动类
3. 反射的使用
详解面试中常考的 Java 反射机制 - 知乎 (zhihu.com) 全!
Java 类的成员包括以下三类:属性字段、构造函数、方法。反射的 API 也是与这几个成员相关:
- Field 类:提供有关类的属性信息,以及对它的动态访问权限。它是一个封装反射类的属性的类。
- Constructor 类:提供有关类的构造方法的信息,以及对它的动态访问权限。它是一个封装反射类的构造方法的类。
- Method 类:提供关于类的方法的信息,包括抽象方法。它是用来封装反射类方法的一个类。
- Class 类: 反射的核心类, 表示正在运行的 Java 应用程序中的类的实例。
1)获取 Class 对象的三种方式
-
第一种方法是通过类的全限定名获取 Class 对象,这也是我们平时最常用的反射获取 Class 对象的方法;
-
第二种方法有限制条件:需要导入类的包;
-
第三种方法已经有了 Student 对象,不再需要反射。
// 1.通过字符串获取Class对象,这个字符串必须带上完整路径名 Class studentClass = Class.forName("com.test.reflection.Student"); // 2.通过类的class属性 Class studentClass2 = Student.class; // 3.通过对象的getClass()函数 Student studentObject = new Student(); Class studentClass3 = studentObject.getClass();
⚠️通过这三种方式获取的 Class 对象是同一个,也就是说 Java 运行时,每一个类只会生成一个 Class 对象。
{ OK,拿到 Class 对象之后,我们就可以为所欲为啦! }
2)获取成员变量
-
获取字段有两个 API:
getDeclaredFields
和getFields
。他们的区别是:getDeclaredFields
用于获取所有声明的字段,包括公有字段和私有字段,getFields
仅用来获取公有字段// 1.获取所有声明的字段 Field[] declaredFieldList = studentClass.getDeclaredFields(); // 2.获取所有公有的字段 Field[] fieldList = studentClass.getFields();
3)获取构造方法
-
获取构造方法同样包含了两个 API:用于获取所有构造方法的
getDeclaredConstructors
和用于获取公有构造方法的getConstructors
// 1.获取所有声明的构造方法 Constructor[] declaredConstructorList = studentClass.getDeclaredConstructors(); // 2.获取所有公有的构造方法 Constructor[] constructorList = studentClass.getConstructors();
4)获取非构造方法
-
同样地,获取非构造方法的两个 API 是:获取所有声明的非构造函数的
getDeclaredMethods
和仅获取公有非构造函数的getMethods
// 1.获取所有声明的函数 Method[] declaredMethodList = studentClass.getDeclaredMethods(); // 2.获取所有公有的函数 Method[] methodList = studentClass.getMethods();
5) method.invoke() 调用反射对象的方法
//使用反射获取的Method对象(Method xxmethod)调用反射对象的方法
xxmethod.invoke(studentClass, 对应方法的参数);
6) 反射使用的例子
Class actionClass=Class.forName(“MyClass”); //获取Class对象
Object action=actionClass.newInstance(); //根据Class对象获取反射类对象
Method method = actionClass.getMethod(“myMethod”,null); //获取反射类对象的方法
method.invoke(action,null); //调用该方法
八、注解机制
1. 什么是注解
Annotation
(注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
2. 注解的常见分类
-
Java自带的标准注解,包括
@Override
、@Deprecated
和@SuppressWarnings
,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。 -
元注解,元注解是用于定义注解的注解,(包括
@Retention
、@Target
、@Inherited
、@Documented
),@Retention
用于标明注解保留的时间范围(源文件保留、编译器保留、运行期保留三种),@Target
用于标明注解使用的范围,@Inherited
用于标明注解可继承,@Documented
用于标明是否生成javadoc文档。Java8还新增了 @Repeatable,@Native -
自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。
看一下 @Override 注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { } //从定义可以看到,这个注解可以被用来修饰方法,并且它只在编译时有效,在编译后的class文件中便不再存在。 //这个注解的作用:告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,若发现父类中不存在这个方法或是存在的方法签名不同,则会报错。
3. 注解的解析方法
- 编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 - 运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。
九、SPI机制
1. 什么是SPI
-
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,
即, 由接口调用方确定接口规则,然后由不同的厂商去根绝这个规则对这个接口进行实现,从而提供服务。 专门提供给 服务提供者或者扩展框架功能的开发者 去使用的一个接口。
-
主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对这个同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为这个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。
-
SPI 将服务接口和具体的服务实现分离开,将服务调用方和服务实现者解耦,提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
-
使用 Java 的 SPI 机制的框架,eg:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等。
2. SPI机制怎么用
-
有关组织或者公司定义标准。
-
具体厂商或者框架开发者实现。
-
程序猿使用。
-
例:
-
定义标准,就是定义接口。比如接口
java.sql.Driver
-
厂商或者框架开发者开发具体的实现:
在
META-INF/services
目录下定义一个名字为接口全限定名的文件,比如java.sql.Driver
文件,文件内容是具体的实现名字,比如me.cxis.sql.MyDriver
。写具体的实现
me.cxis.sql.MyDriver
,是对接口Driver的实现- 程序员 引用具体厂商的jar包来实现我们的功能
-
3. SPI和API有什么区别
在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
-
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
-
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
4. SPI 的优缺点?
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个
ServiceLoader
同时load
时,会有并发问题
十、序列化
1. Java序列化与反序列化是什么?
-
在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们**创建出来的这些Java对象都是存在于JVM的堆内存中的。**只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。
-
在真实应用场景中 , 需要持久化 Java 对象【比如将 Java 对象保存在文件中,或者在网络传输 Java 对象】,这些场景都需要用到序列化。
-
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
-
序列化: 将数据结构或对象转换成二进制字节流的过程。( 以便在网络上传输或者保存在本地文件中)
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
-
反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
2. 如何实现Java序列化
Java与序列化和反序列化有关的API
java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream
-
法一:实现 java.io.Serializable 接口
-
需要被序列化的类必须实现Serializable接口。Serializable序列化接口里面没有方法或字段,什么内容都没有,我们可以将它理解成一个标识接口,仅用于标识可序列化的语义。在Java中的这个Serializable接口其实是给jvm看的,通知jvm,你(jvm)帮我序列化。
-
当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象, 将抛出
NotSerializableException
。 因为序列化在真正的执行过程中会使用instanceof判断一个类是否实现Serializable接口 -
可序列化类的所有子类型本身都是可序列化的。 如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该集成
java.io.Serializable
接口。//1.一个实现Serializable接口的类 @Data public class User1 implements Serializable { private String name; private int age; } //2.进行序列化和反序列化操作 public class SerializableDemo1 { public static void main(String[] args) { //Initializes The Object User1 user = new User1(); user.setName("hollis"); user.setAge(23); System.out.println(user); //Write Obj to File try (FileOutputStream fos = new FileOutputStream("tempFile"); ObjectOutputStream oos = new ObjectOutputStream( fos)) { oos.writeObject(user); } catch (IOException e) { e.printStackTrace(); } //Read Obj from File File file = new File("tempFile"); try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { User1 newUser = (User1)ois.readObject(); System.out.println(newUser); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } //OutPut: //User{name='hollis', age=23} //User{name='hollis', age=23} //对象的属性均被持久化了下来 //若User1类没有implements Serializable 跑该代码就会报错`NotSerializableException`
-
-
实现 java.io. Externalizable 接口
//1.一个实现Serializable接口的类 @Data public class User1 implements Externalizable { private String name; private int age; @Override public void writeExternal(ObjectOutput out) throws IOException { } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { } } //2.进行序列化和反序列化操作 public class ExternalizableDemo1 { public static void main(String[] args) { User1 user = new User1(); user.setName("hollis"); user.setAge(23); System.out.println(user); //Write Obj to file try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"))){ oos.writeObject(user); } catch (IOException e) { e.printStackTrace(); } //Read Obj from file File file = new File("tempFile"); try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))){ User1 newInstance = (User1) ois.readObject(); //output System.out.println(newInstance); } catch (IOException | ClassNotFoundException e ) { e.printStackTrace(); } } } //output //User{name='hollis', age=23} //User{name='null', age=0}
-
发现,对User1类进行序列化及反序列化之后得到的对象的所有属性的值都变成了默认值。也就是说,之前的那个对象的状态并没有被持久化下来。
-
Externalizable接口和Serializable接口的区别: Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法, 否则所有变量的值都会变成默认值 。
-
⚠️在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。 如果实现了Externalizable接口的类中没有无参数的构造函数,在运行时会抛出异常:java.io.InvalidClassException。
-
如果一个Java类没有定义任何构造函数,编译器会帮我们自动添加一个无参的构造方法,可是,如果我们在类中定义了一个有参数的构造方法了,编译器便不会再帮我们创建无参构造方法,这点需要注意。
//修改User1 implements Externalizable代码,实现那两个函数 @Data public class User1 implements Externalizable { private String name; private int age; @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); age = in.readInt(); } } //再测试就可以持久化保存了,输出结果是正确的
-
两种序列化方式对比
实现Serializable接口 实现Externalizable接口 系统自动存储必要的信息 程序员决定存储哪些信息 Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 必须实现接口内的两个方法 性能略差 性能略好
3. 有些字段不想进行序列化- transient
对于不想进行序列化的变量,使用 transient
关键字修饰。
private transient String content;
-
transient
关键字的作用是: 控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后, 被 transient 修饰的变量值被设为初始值,不会被持久化和恢复,如 int 型的是 0,对象型的是 null。 -
关于
transient
还有几点注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
4. 被transient关键字修饰的变量不一定不能被序列化
-
若实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal()方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关。
即若实现的是Externalizable接口,在writeExternal()方法中指定要序列化一个被transient修饰的变量,那么它还是会被序列化
-
在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。
也就是说,用户自定义的 writeObject() 和 readObject() 方法可以允许用户控制序列化的过程,比如让使用transitent定义的变量也可以持久化保存,可以在序列化的过程中动态改变序列化的数值。
-
例:【ArrayList源码】中elementData用来存数据,虽然被声明为transitent,但是有这两个方法的实现,那么elementData能被序列化持久下来。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; transient Object[] elementData; // non-private to simplify nested class access private int size; …… private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { …… } private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ …… } }
-
理解:ArrayList为什么要用这种方式来实现序列化?
-
ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,优化存储,ArrayList把元素数组设置为transient。
-
但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写
writeObject
和readObject
方法的方式把其中的元素保留下来。writeObject
方法把elementData
数组中的元素遍历的保存到输出流(ObjectOutputStream)中。readObject
方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData
数组中。
-
-
5. serialversionUID 变量
private static final long serialVersionUID = 1L;
-
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化ID,就是在代码中定义的
serialVersionUID
。 -
因为,在进行反序列化时,JVM会把(文件/网络)传来的字节流中的
serialVersionUID
与本地相应实体类的serialVersionUID
进行比较,如果相同就认为是一致的,可以进行反序列化;否则就会出现序列化版本不一致的异常,即是InvalidCastException
。 -
《阿里巴巴Java开发手册》中规定,在兼容性升级中,修改类的时候,不要修改
serialVersionUID
的原因。 除非是完全不兼容的两个版本。
所以,serialVersionUID
其实是验证版本一致性的。 所以在做兼容性升级的时候,不要改变类中serialVersionUID
的值。 在做版本升级的时候(非兼容性升级),记得要修改这个字段的值
-
一旦类实现了
Serializable
,就建议明确的定义一个serialVersionUID
。若不明确定义,系统会自己添加一个默认的serialVersionUID
,然后在修改类的时候,就会导致系统自定义的UID版本在改前改后不一致发生 InvalidClassException 异常。
11、I/O
【IO这块还得细看】
1. Java的IO 流分为几种?
-
按照流的方向:输入流(inputStream)和输出流(outputStream);
-
按照实现功能分:节点流(可以从或向一个特定的地方读写数据,如 FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写, BufferedReader);
-
按照处理数据的单位: 字节流和字符流。分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个):InputStream,OutputStream,Reader,Writer。Java中其他多种多样变化的流均是由它们派生来的。
2. 字符流与字节流的区别?
- 读写的时候字节流是按字节读写,字符流按字符读写。
- 字节流适合所有类型文件的数据传输,因为计算机字节(Byte)是电脑中表示信息含义的最小单位。字符流只能够处理纯文本数据,其他类型数据不行,但是字符流处理文本要比字节流处理文本要方便。
- 在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。
- 只是读写文件,和文件内容无关时,一般选择字节流。
3. BIO、NIO、AIO的区别?
- BIO:同步并阻塞,在服务器中实现的模式为一个连接一个线程。也就是说,客户端有连接请求的时候,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这也可以通过线程池机制改善。BIO一般适用于连接数目小且固定的架构,这种方式对于服务器资源要求比较高,而且并发局限于应用中,是JDK1.4之前的唯一选择,但好在程序直观简单,易理解。
- NIO:同步并非阻塞,在服务器中实现的模式为一个请求一个线程,也就是说,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程进行处理。NIO一般适用于连接数目多且连接比较短(轻操作)的架构,并发局限于应用中,编程比较复杂,从JDK1.4开始支持。
- AIO:异步并非阻塞,在服务器中实现的模式为一个有效请求一个线程,也就是说,客户端的IO请求都是通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作系统参与并发操作,编程比较复杂,从JDK1.7开始支持。
4. Java IO都有哪些设计模式?
使用了适配器模式和装饰器模式
12、语法糖
1. 什么是语法糖?
语法糖(Syntactic sugar) 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
eg:Java 中的 for-each
就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。
String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"};
for (String s : strs) {
System.out.println(s);
}
- JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。
- 如果你去看
com.sun.tools.javac.main.JavaCompiler
的源码,你会发现在compile()
中有一个步骤就是调用desugar()
,这个方法就是负责解语法糖的实现的。
2. Java 中有哪些常见的语法糖?
Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。 Java 语法糖详解 | JavaGuide