赞
踩
参考:JavaGuide,www.javalearn.cn, www.pdai.tech等
指针。 Java 没有指针的概念。在 C/C++中,指针操作内存时,经常会出现错误。 Java没有指针更安全
多重继承 。C++支持多继承,而Java不支持多重继承,但允许一个类实现多个接口
数据类型。
自动内存管理 。 Java 自动进行无用内存回收操作,不再需要程序员进行手动删除,而 C++ 中必须由程序释放内存资源
操作符重载。 Java 不支持操作符重载,操作符重载则被认为是 C++ 的突出特征 (不过 Java语言还是可以通过类来实现操作符重载所具有的功能的 )
预处理功能 。
缺省参数函数 。 Java 不支持缺省参数函数,而 C++支持 。( 所谓缺省参数,顾名思义,就是在声明函数的某个参数的时候为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就无须指定该参数。)
字符串 。
goto 语句。 goto 语句是 C 和 C++ 的“遗物”,Java 不提供 goto 语句,虽然 Java 指定 goto 作为关键字,但不支持它的使用,这使程序更简洁易读
类型转换 。
面向对象(封装,继承,多态)。 Java 语言提供类、接口和继承等面向对象的特性
平台无关性。Java的虚拟机机制,使Java“一次编写,随处运行(Write once, Run anywhere)”,在不同平台上运行不需要重新编译
支持多线程。C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持;
支持网络编程并且很方便。Java 语言诞生本身就是为简化网络编程设计的
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
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。
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 许可获得许可。
分类 | 关键字 | ||||||
---|---|---|---|---|---|---|---|
访问控制 | 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 |
浅析Java中的final关键字 - Matrix海子 - 博客园 (cnblogs.com) 绝!
上面这篇文章问到一个问题为什么匿名内部类可以访问的外部成员必须是final修饰的,拓展理解Java四种内部类
Java内部类详解 - Matrix海子 - 博客园 (cnblogs.com)
Static静态代码块以及各代码块之间的执行顺序 - 掘金 (juejin.cn)
Java 中 this 和 super 的用法总结 | 菜鸟教程 (runoob.com)
Java关键字(一)——instanceof - YSOcean - 博客园 (cnblogs.com)
Java关键字(二)——native - YSOcean - 博客园 (cnblogs.com)
Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)_老鼠只爱大米的博客-CSDN博客_java的volatile
Java中synchronized关键字作用及用法_江湖人称小程的博客-CSDN博客
语法形式 :从语法形式看,成员变量属于类,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及 static
所修饰;但是,成员变量和局部变量都能被 final
所修饰。
存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用 static
修饰的,那么这个成员变量是属于类的,如果没有使用 static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。(堆栈都放什么???)
生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
默认值 :从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
静态变量可以被类的所有实例共享。无论一个类创建了多少个对象,它们都共享同一份静态变量。
通常情况下,静态变量会被 final
关键字修饰成为常量
char
在 Java 中占两个字节类名.方法名
的方式,也可以使用 对象.方法名
的方式 。但实例方法只能用对象.方法名
的方式。( 注意的是一般不建议使用 对象.方法名
的方式来调用静态方法。 )重载就是同一个类中多个同名方法,根据不同的传参来执行不同的逻辑处理。
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法 ,( 重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。 ) 重写发生在运行期
方法名和参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类 (重写要遵循“两同两小一大” )
(关于 重写的返回值类型 这里需要额外多说明一下:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。 )
如果父类方法访问修饰符为 private/final/static
则子类就不能重写该方法,但是被 static
修饰的方法能够被再次声明。
构造方法无法被重写
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
public static void method1(String arg1, String... args) {
//......
}
class
文件就可以看出来了值传递:指的是在方法调用时,传递的参数是值的拷贝,也就是说传递后就互不相关了。
引用传递:指的是在方法调用时,传递的参数是按引用进行传递,传递的是引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(即同一个内存空间)。
基本类型作为参数被传递时肯定是值传递;引用类型作为参数被传递时也是值传递,“值”为对应的引用。
6 种数字类型:
byte
、short
、int
、long
float
、double
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++中的指针类型、结构类型、联合类型和枚举类型。
成员变量包装类型不赋值就是 null
,而基本类型有默认值且不是 null
。
包装类型可用于泛型,而基本类型不可以
存放位置。基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static
修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,而几乎所有对象实例都存在于堆中。
相比于对象类型, 基本数据类型占用的空间非常小。
⚠️ 注:什么时候该用包装类,什么时候用基本类型,看基本的业务来定:这个字段允不允许null值,如果允许null值,则必然要用封装类,否则值类型就可以了,用到比如泛型和反射调用函数.,就需要用包装类!
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
Integer i = 10; //装箱
int n = i; //拆箱
原理:看上面两行代码的对应的.class文件可以发现, 装箱其实就是调用了 包装类的valueOf()
方法,拆箱其实就是调用了 xxxValue()
方法。 即
Integer i = 10
等价于 Integer i = Integer.valueOf(10)
int n = i
等价于 int n = i.intValue()
;⚠️注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作
前提:发生在自动装箱的过程(基本类型–>包装类型)
机制:当通过自动装箱机制创建包装类对象时,首先会判断数值是否在-128—-127的范围内,如果满足条件,则会从缓存(常量池)中寻找指定数值,若找到缓存,则不会新建对象,只是指向指定数值对应的包装类对象,否则,新建对象。
原理:(看Integer缓存的源码)
作用:
⚠️注意:
不是所有的包装类都有缓存机制。( Float,Double,Boolean 三大包装类并没有缓冲机制 )Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
所有整型包装类对象之间值的比较,全部使用equals方法比较。
(因为:在[-128,127] 区间内的会复用已有的对象,而区间外的都会在堆上产生不会复用已有的对象)
数据在内存(计算机)中是以二进制的形式存在的
十进制小数转换成二进制时,有可能会取不尽。( 十进制数的二进制表示形式可能不精确。 )
比如: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类型其实是一样的,区别在于有效位数。
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。通常大部分需要浮点数精确运算结果的业务场景(eg涉及到钱的场景)都是通过 BigDecimal
来做的。实际开发中不建议直接使用float和double进行运算
在使用 BigDecimal
时,为了防止精度丢失,推荐使用它的BigDecimal(String val)
构造方法或者 BigDecimal.valueOf(double val)
静态方法来创建对象。
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包
面向过程是 是一种以过程为中心的编程思想,直接将解决问题的步骤分析出来,然后用函数把步骤一步一步实现,然后再依次调用;
而面向对象是将构成问题的事物, 抽象出对象,然后用对象执行方法的方式解决问题。
面向过程编程,数据和对数据的操作是分离的。 面向对象编程,数据和对数据的操作是绑定在一起的。
封装 : 指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
继承: 它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。
⚠️
多态: 它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
简单的说:就是用基类的引用指向子类的对象。
⚠️
什么是多态机制:一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须由程序运行期间才能决定,这就是多态性
多态性可以分为编译时多态和运行时多态。
多态如何实现
Java实现多态有三个必要条件:继承、重写(覆盖)、向上转型
new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
一个对象实例可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
对象的相等一般比较的是内存中存放的内容是否相等。引用相等一般比较的是他们指向的内存地址是否相等
语法层面上的区别:
设计层面上的区别:
抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。
抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
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();
引用拷贝: 创建一个指向对象的引用变量的拷贝。 即两个不同的引用指向同一个对象
对象拷贝: 创建对象本身的一个副本。 深拷贝和浅拷贝都是对象拷贝
浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。
简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
深拷贝:是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
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 { }
==
对于基本类型和引用类型的作用效果是不同的:
对于基本数据类型来说,==
比较的是值。
对于引用数据类型来说,==
比较的是对象的内存地址。
⚠️因为 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 对象。
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方法。这显然会大大降低效率。
所以:
当你把对象加入 HashSet
时,HashSet
会先计算对象的 hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode
值作比较。
如果没有相符的 hashCode
,HashSet
会假设对象没有重复出现。
但是如果发现有相同 hashCode
值的对象,这时会调用 equals()
方法来检查 hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。
这样我们就大大减少了 equals
的次数,相应就大大提高了执行速度。 hashCode
大大缩小了查找成本。
如果两个对象的**hashCode
值相等,那这两个对象不一定相等**(哈希碰撞)。
如果两个对象的hashCode
值相等并且equals()
方法也返回 true
,我们才认为这两个对象相等。
如果两个对象的hashCode
值不相等,我们就可以直接认为这两个对象不相等。
⚠️因为 hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode
)。
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
equals()
时没有重写 hashCode()
方法的话,会出现什么问题?重写了equals方法,不重写hashCode方法时,可能会出现equals方法返回为true,而hashCode方法却返回不同的结果。
影响: 在java底层的集合框架中(如HashMap,HashSet等),为了提高查询的效率,在确定某个对象的存储位置时,往往需要通过首先调用对象的hashCode方法来实现。 若涉及到判断两个对象是否相等时,重写了equals()方法,二者的equals()后判断对象相同,但是java底层在实现时会先调用hashCode方法,因为没有重写,返回是不一样的,就会造成判断对象不同,判断错误。
可变性。
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
看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不可变的主要原因是
从缓存池角度看——String缓存池
String Pool 是在方法区的一块特殊存储区域。当一个String被创建时如果发现当前String已经存在于String Pool,则会返回一个已存在String的引用而不会新建一个对象。
以下代码只会创建一个String对象在堆内存中。
String s1 = "hello";
String s2 = "hello";
在缓存池中,如果一个String是可变的,改变了一个引用指向的String,那么就会导致其他引用得到错误的值。因此String不可被改变。
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
String a = “aaa” ;
,程序运行时会在常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。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
StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。StringBuilder
以复用,会导致创建过多的 StringBuilder
对象。 如果直接使用 StringBuilder
对象进行字符串拼接的话,就不会存在这个问题了。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异常类层次结构图
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
Exception
:程序本身可以处理的异常,可以通过 catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
Error
:Error
属于程序无法处理的错误 ,我们没办法通过 catch
来进行捕获 。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复。
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch
或者throws
关键字处理的话,就没办法通过编译。
常见的受检查异常有: IO 相关的异常、ClassNotFoundException
、SQLException
…。
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException
及其子类都统称为非受检查异常,常见的有:
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户)非受检查异常和受检查异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检查异常,否则就选择非受检查异常。
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;
}
异常捕获处理的方法通常有:
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 返回值。
不一定。在某些情况下,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");
}
面对必须要关闭的资源,我们总是应该优先使用 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’。
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 ; // 此变量的类型由外部决定
……
}
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>("汤姆") ; // 通过子类实例化对象
定义泛型方法语法格式
说明一下,定义泛型方法时,必须在返回值前边加一个< T>,来声明这是一个泛型方法,持有一个泛型T
修饰符 <T,E,…> 返回值类型 方法名(形参列表){
……
}
//形参列表通常要有一个与T相关的参数,这样才能在调用的时候指定T
限定通配符包括两种:
非限定通配符:类型为,可以用任意类型来替代。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java的反射机制。Java反射机制在框架设计中极为广泛。
but, 使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
注解 的实现也用到了反射。
为什么你使用 Spring 的时候 ,一个@Component
注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value
注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。
jdbc就是典型的反射
Class.forName('com.mysql.jdbc.Driver.class');//加载MySQL的驱动类
详解面试中常考的 Java 反射机制 - 知乎 (zhihu.com) 全!
Java 类的成员包括以下三类:属性字段、构造函数、方法。反射的 API 也是与这几个成员相关:
第一种方法是通过类的全限定名获取 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 对象之后,我们就可以为所欲为啦! }
获取字段有两个 API:getDeclaredFields
和getFields
。他们的区别是:
getDeclaredFields
用于获取所有声明的字段,包括公有字段和私有字段,getFields
仅用来获取公有字段
// 1.获取所有声明的字段
Field[] declaredFieldList = studentClass.getDeclaredFields();
// 2.获取所有公有的字段
Field[] fieldList = studentClass.getFields();
获取构造方法同样包含了两个 API:用于获取所有构造方法的 getDeclaredConstructors
和用于获取公有构造方法的getConstructors
// 1.获取所有声明的构造方法
Constructor[] declaredConstructorList = studentClass.getDeclaredConstructors();
// 2.获取所有公有的构造方法
Constructor[] constructorList = studentClass.getConstructors();
同样地,获取非构造方法的两个 API 是:获取所有声明的非构造函数的 getDeclaredMethods
和仅获取公有非构造函数的 getMethods
// 1.获取所有声明的函数
Method[] declaredMethodList = studentClass.getDeclaredMethods();
// 2.获取所有公有的函数
Method[] methodList = studentClass.getMethods();
//使用反射获取的Method对象(Method xxmethod)调用反射对象的方法
xxmethod.invoke(studentClass, 对应方法的参数);
Class actionClass=Class.forName(“MyClass”); //获取Class对象
Object action=actionClass.newInstance(); //根据Class对象获取反射类对象
Method method = actionClass.getMethod(“myMethod”,null); //获取反射类对象的方法
method.invoke(action,null); //调用该方法
Annotation
(注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。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文件中便不再存在。
//这个注解的作用:告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,若发现父类中不存在这个方法或是存在的方法签名不同,则会报错。
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。@Value
、@Component
)都是通过反射来进行处理的。SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,
即, 由接口调用方确定接口规则,然后由不同的厂商去根绝这个规则对这个接口进行实现,从而提供服务。 专门提供给 服务提供者或者扩展框架功能的开发者 去使用的一个接口。
主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对这个同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为这个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。
SPI 将服务接口和具体的服务实现分离开,将服务调用方和服务实现者解耦,提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
使用 Java 的 SPI 机制的框架,eg:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等。
有关组织或者公司定义标准。
具体厂商或者框架开发者实现。
程序猿使用。
例:
定义标准,就是定义接口。比如接口java.sql.Driver
厂商或者框架开发者开发具体的实现:
在META-INF/services
目录下定义一个名字为接口全限定名的文件,比如java.sql.Driver
文件,文件内容是具体的实现名字,比如me.cxis.sql.MyDriver
。
写具体的实现me.cxis.sql.MyDriver
,是对接口Driver的实现
在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
ServiceLoader
同时 load
时,会有并发问题在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们**创建出来的这些Java对象都是存在于JVM的堆内存中的。**只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。
在真实应用场景中 , 需要持久化 Java 对象【比如将 Java 对象保存在文件中,或者在网络传输 Java 对象】,这些场景都需要用到序列化。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
序列化: 将数据结构或对象转换成二进制字节流的过程。( 以便在网络上传输或者保存在本地文件中)
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
Java与序列化和反序列化有关的API
java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream
需要被序列化的类必须实现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`
//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内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 |
性能略差 | 性能略好 |
对于不想进行序列化的变量,使用 transient
关键字修饰。
private transient String content;
transient
关键字的作用是: 控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后, 被 transient 修饰的变量值被设为初始值,不会被持久化和恢复,如 int 型的是 0,对象型的是 null。
关于 transient
还有几点注意:
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int
类型,那么反序列后结果就是 0
。static
变量因为不属于任何对象(Object),所以无论有没有 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
数组中。
private static final long serialVersionUID = 1L;
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化ID,就是在代码中定义的serialVersionUID
。
因为,在进行反序列化时,JVM会把(文件/网络)传来的字节流中的serialVersionUID
与本地相应实体类的 serialVersionUID
进行比较,如果相同就认为是一致的,可以进行反序列化;否则就会出现序列化版本不一致的异常,即是InvalidCastException
。
《阿里巴巴Java开发手册》中规定,在兼容性升级中,修改类的时候,不要修改serialVersionUID
的原因。 除非是完全不兼容的两个版本。
所以,serialVersionUID
其实是验证版本一致性的。 所以在做兼容性升级的时候,不要改变类中serialVersionUID
的值。 在做版本升级的时候(非兼容性升级),记得要修改这个字段的值
一旦类实现了Serializable
,就建议明确的定义一个 serialVersionUID
。若不明确定义,系统会自己添加一个默认的serialVersionUID
,然后在修改类的时候,就会导致系统自定义的UID版本在改前改后不一致发生 InvalidClassException 异常。
【IO这块还得细看】
按照流的方向:输入流(inputStream)和输出流(outputStream);
按照实现功能分:节点流(可以从或向一个特定的地方读写数据,如 FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写, BufferedReader);
按照处理数据的单位: 字节流和字符流。分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个):InputStream,OutputStream,Reader,Writer。Java中其他多种多样变化的流均是由它们派生来的。
使用了适配器模式和装饰器模式
语法糖(Syntactic sugar) 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
eg:Java 中的 for-each
就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。
String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"};
for (String s : strs) {
System.out.println(s);
}
com.sun.tools.javac.main.JavaCompiler
的源码,你会发现在compile()
中有一个步骤就是调用desugar()
,这个方法就是负责解语法糖的实现的。Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。 Java 语法糖详解 | JavaGuide
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。