Java 基础
2024-03-18 11:51:56 0 举报
AI智能生成
Java是一门面向对象的编程语言,广泛应用于企业级应用开发、网页开发、游戏开发等领域。
作者其他创作
大纲/内容
特点
OOP(封装,继承,多态)。
平台无关性( Java 虚拟机实现平台无关性)。
可靠性(具备异常处理和自动内存管理机制)。
安全性(访问权限修饰符、限制程序直接访问操作系统资源)。
高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的)。
多线程、支持网络编程且很方便、编译与解释并存。
大版本
Java SE(Java Platform,Standard Edition)
适合开发桌面应用程序或简单的服务器应用程序。
Java EE(Java Platform,Enterprise Edition )
适合开发复杂的企业级应用程序或 Web 应用程序。
Java ME(Java Platform,Micro Edition)
主要用于开发嵌入式消费电子设备的应用程序。(淘汰了)
JVM vs JDK vs JRE
JVM(Java Virtual Machine)
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。
JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,都会给出相同的结果。
JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。
JDK(Java Development Kit)
功能齐全的 Java SDK,是提供给开发者使用,能够创建和编译 Java 程序的开发套件。
包含 JRE、码编译器 javac 以及一些其他工具,如 javadoc(文档注释工具)、jdb(调试器)、jconsole(可视化监控⼯具)、javap(反编译工具)等。
JRE(Java Runtime Environment)
Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。
从 JDK 9 开始就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统(JDK 被重新组织成 94 个模块)+ jlink 工具。JDK 11 不单独提供 JRE 下载。
可以用 jlink 根据自己的需求,创建一个更小的 runtime(运行时),而不是不管什么应用,都是同样的 JRE。
源码编译过程
字节码
JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。
.class->机器码
JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。
编译模式
JIT
JIT(Just in Time Compilation)属于运行时编译,解决热点代码(经常使用的代码)频繁重复编译问题,从而提高性能。
原理
当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。
优点
具备更高的极限处理能力,可以降低请求的最大延迟。
AOT
AOT(Ahead of Time Compilation) 在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。
AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。
AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。
优点
优势在于启动时间、内存占用和打包体积。
缺点
无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。
应用
GraalVM
一种高性能的 JDK(完整的 JDK 发行版本),它可以运行 Java 和其他 JVM 语言,以及 JavaScript、Python 等非 JVM 语言。
GraalVM 不仅能提供 AOT 编译,还能提供 JIT 编译。
编译与解释并存
编译型
通过编译器将源代码一次性翻译成可被该平台执行的机器码。
编译语言的执行速度比较快,开发效率比较低。
常见的编译性语言有 C、C++、Go、Rust 等等。
解释型
通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。
解释型语言开发效率比较快,执行速度比较慢。
常见的解释性语言有 Python、JavaScript、PHP 等等。
即时编译
为了改善编译语言的效率而发展出的即时编译技术,已经缩小了编译型和解释型语言间的差距。
这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。
Java 与 LLVM 是这种技术的代表产物。
Oracle JDK vs OpenJDK
2006 年 SUN 公司将 Java 开源,也就有了 OpenJDK(完全免费,GPL v2 许可协议)。
2009 年 Oracle 收购了 Sun 公司,在 OpenJDK 的基础上搞了一个 Oracle JDK(部分(短期)免费,BCL/OTN 许可协议)。
建议选择 OpenJDK 或者基于 OpenJDK 的发行版,比如 AWS 的 Amazon Corretto,阿里巴巴的 Alibaba Dragonwell。
基本语法
注释
单行注释、多行注释、文档注释。
用的比较多的还是单行注释和文档注释,多行注释在实际开发中使用的相对较少。
注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释)。
代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。
若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。
若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。
标识符
标识符就是为程序、类、变量、方法等取的名字 。
关键字
被赋予特殊含义的标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方。
continue、break 和 return
continue:指跳出当前的这一次循环,继续下一次循环。
break:指跳出整个循环体,继续执行循环下面的语句。
return
用于跳出所在方法,结束该方法的运行。
return;:直接使用 return 结束方法执行,用于没有返回值函数的方法。
return value;:return 一个特定值,用于有返回值函数的方法。
注意
default 这个关键字很特殊,既属于程序控制,也属于类,方法和变量修饰符,还属于访问控制。
虽然 true, false, 和 null 看起来像关键字但实际上他们是字面值,同时你也不可以作为标识符来使用。
运算符
自增自减运算符
自增运算符(++)、自减运算符(--)
++ 和 -- 运算符可以放在变量之前或变量之后,当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。
移位运算符
移位运算符是最基本的运算符之一,几乎每种编程语言都包含这一运算符。
移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。
在 Java 代码里使用 <<、 >> 和 >>> 转换成的指令码运行起来会更高效些。
分类
<< :左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。
>> :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。
>>> :无符号右移,忽略符号位,空位都以 0 补齐。
注意
由于 double,float 在二进制中的表现比较特殊,因此不能来进行移位操作。
移位操作符实际上支持的类型只有 int 和 long,编译器在对short、byte、char类型进行移位前,都会将其转换为int类型再操作。
当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。即 x<<42 等同于 x<<10。
new 运算符
new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
基本数据类型
基本类型
数字类型
整数型
byte、short、int、long
Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析。
BigInteger
用于超过 long 整型数据的数据类型。
原理
内部使用 int[] 数组来存储任意大小的整形数据。
缺点
相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。
浮点型
float、double
精度丢失
无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
解决
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。
防止精度丢失
BigDecimal(String val)
BigDecimal.valueOf(double val)
equals(),比较精度
compareTo(),忽略精度
字符类型
char
char a = 'h'char :单引号,String a = "hello" :双引号。
布尔型
boolean
依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。
默认值及所占空间
字节 vs 位数
位数(bit):最小存储单位。
字节(byte):由8个bit组成。
Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。
包装类型
包装类
Byte、Short、Integer、Long、Float、Double、Character、Boolean 。
缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
缓存范围
Byte,Short,Integer,Long 默认创建了数值 [-128,127] 的相应类型的缓存数据。
Character 创建了数值在 [0,127] 范围的缓存数据。
Boolean 直接返回 True or False。
两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。
比较
范围内==比较相等,范围外==比较不等,推荐equals比较。
自动拆装箱
装箱
将基本类型用它们对应的引用类型包装起来。
原理
调用了包装类的 valueOf() 方法。
拆箱
将包装类型转换为基本数据类型。
原理
调用了包装类的 xxxValue() 方法。
注意
如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
基本类型和包装类型
用途:除了定义一些常量和局部变量外,在其他比如方法参数、对象属性中很少会使用基本类型来定义变量。包装类型可用于泛型,而基本类型不可以。
存储方式:基本数据类型的存放在栈中(局部变量)或存放在堆中(成员变量(未被 static 修饰 ))。包装类型属于对象类型,几乎都存在于堆中。
占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
比较方式:对基本数据类型来说,== 比较的是值。对包装数据类型来说,== 比较的是对象的内存地址。包装类对象之间值的比较,全部使用 equals() 方法。
变量
成员变量与局部变量
语法形式:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;
成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但成员变量和局部变量都能被 final 所修饰。
成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但成员变量和局部变量都能被 final 所修饰。
存储方式:如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,
如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
生存时间:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
默认值:成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(被 final 修饰的成员变量必须显式地赋值),而局部变量则不会自动赋值。
静态变量
被 static 关键字修饰的变量,可以被类的所有实例共享。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
字符型常量和字符串常量
形式:字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
含义:字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)。
内存大小:字符常量只占 2 个字节;字符串常量占若干个字节。
方法
方法的返回值
获取到的某个方法体中的代码执行后产生的结果。
静态方法和实例方法
调用方式
在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式(不推荐),而实例方法只有 对象.方法名 这种方式。
访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员,不允许访问实例成员,而实例方法不存在这个限制。
重载和重写
重载
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
重写
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
规则
方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
构造方法无法被重写。
构造方法
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。
一个类即使没有声明构造方法也会有默认的无参构造方法。但若手动添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参构造方法了。
特点
名字与类名相同。
没有返回值,但不能用 void 声明构造函数。
生成类的对象时自动执行,无需调用。
不能被 override(重写),但可以 overload(重载)。
参数
实参&形参
实参(实际参数,Arguments)
用于传递给函数/方法的参数,必须有确定的值。
形参(形式参数,Parameters)
用于定义函数/方法,接收实参,不需要有确定的值。
值传递&引用传递
值传递
方法接收的是实参值的拷贝,会创建副本。
引用传递
方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,在 Java 中只有值传递(创建地址副本)。
可变长参数
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。
格式
method(String... args)
原理
Java 的可变参数编译后实际会被转换成一个数组。
注意
优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
OOP
面向对象和面向过程
面向对象:会先抽象出对象,然后用对象执行方法的方式解决问题。
面向过程:把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
面向对象开发的程序一般更易维护、易复用、易扩展。
特征
封装
把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息,但提供一些可以被外界访问的方法来操作属性。
继承
使用已存在的类的定义作为基础建立新类,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
特点
子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
多态
一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
特点
对象类型和引用类型之间具有继承(类)/实现(接口)的关系。
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定。
多态不能调用“只在子类存在但在父类不存在”的方法。
如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
接口和抽象类
共同点
都不能被实例化。
都可以包含抽象方法。
都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
区别
接口主要用于对类的行为进行约束,实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
一个类只能继承一个类,但是可以实现多个接口。
接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义或被重新赋值。
深拷贝和浅拷贝、引用拷贝
浅拷贝
浅拷贝会在堆上创建一个新对象,但原对象内部的属性是引用类型,浅拷贝会直接复制内部对象的引用地址,也就是拷贝对象和原对象共用同一个内部对象。
深拷贝
深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
Object
Object
Object 类是一个特殊的类,是所有类的父类。它主要提供了 11 个方法。
方法
public final native Class<?> getClass();
native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
public native int hashCode();
native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
public boolean equals(Object obj);
用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException;
native 方法,用于创建并返回当前对象的一份拷贝。
native 方法,用于创建并返回当前对象的一份拷贝。
public String toString();
返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public final native void notify();
native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll();
native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException;
native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException;
多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。
多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。
public final void wait() throws InterruptedException;
跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。
跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。
protected void finalize() throws Throwable { }
实例被垃圾回收器回收的时候触发的操作。
实例被垃圾回收器回收的时候触发的操作。
== 和 equals()
==
基本数据类型比较的是值。
引用数据类型比较的是对象的内存地址。
equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
没重写:比较的是对象的内存地址。重写:比较属性或自定义内容。自定义类推荐重写此方法。
hashCode()
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的。
hashCode() 和 equals() 都是用于比较两个对象是否相等。(自定义类推荐重写方法)
特点
如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
如果两个对象的hashCode 值相等并且 equals() 方法也返回 true,我们才认为这两个对象相等。
如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
String
不可变性
保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
应用
String:操作少量的数据。
StringBuilder:单线程操作字符串缓冲区下操作大量数据。
StringBuffer:多线程操作字符串缓冲区下操作大量数据。
底层实现
Java 9 以前,底层由 char[] 实现。
Java 9 之后,底层由 byte[] 实现。
原因
Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String#intern
一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中。
常量折叠
常量折叠(Constant Folding),把常量表达式的值求出来作为常量嵌在最终生成的代码中的编译优化。
这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以折叠。
可折叠
基本数据类型(byte、boolean、short、char、int、float、long、double)以及字符串常量。
final 修饰的基本数据类型和字符串变量。
字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
注意
在 JDK9 当中,字符串相加 “+” 改为了用动态方法,不再影响性能了。
异常
java.lang.Throwable
Java 异常类顶级父类。
方法
String getMessage():返回异常发生时的简要描述。
String toString():返回异常发生时的详细信息。
String getLocalizedMessage():返回异常对象的本地化信息。
使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同。
使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同。
void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息。
Exception
程序本身可以处理的异常,可以通过 catch 来进行捕获。
分类
Checked Exception
受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch 或者 throws 关键字处理的话,就没办法通过编译。
Unchecked Exception
不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException 及其子类都统称为非受检查异常。
常见
NullPointerException(空指针错误)
IllegalArgumentException(参数错误比如方法入参类型错误)
NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
ArrayIndexOutOfBoundsException(数组越界错误)
ClassCastException(类型转换错误)
ArithmeticException(算术错误)
SecurityException (安全错误比如权限不够)
UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
Error
Error 属于程序无法处理的错误 ,不建议通过catch捕获 。
Error 发生时,Java 虚拟机(JVM)一般会选择线程终止。
异常捕获
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 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。
因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行 finally 语句中的 return 之后,本地变量的值就变为 finally 语句中的 return 返回值。
finally 不是一定会执行
程序所在的线程死亡。
关闭 CPU。
try-with-resources
适用
任何实现 java.lang.AutoCloseable 或者 java.io.Closeable 的对象。
关闭资源和 finally 块的执行顺序
在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行。
注意
不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
抛出的异常信息一定要有意义。
建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出 NumberFormatException 而不是其父类 IllegalArgumentException。
使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。
泛型(Generics)
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。
使用方式
泛型类
public class Generic<T> { private T key; }
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
在实例化泛型类时,必须指定T的具体类型。
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
在实例化泛型类时,必须指定T的具体类型。
泛型接口
public interface Generator<T> { public T method(); }
泛型方法
public static <E> void printArray( E[] inputArray ) { }
作用
使⽤泛型可在编译期间进⾏类型检测。
使⽤ Object 类型需要⼿动添加强制类型转换,降低代码可读性,提⾼出错概率。
泛型可以使⽤⾃限定类型,如 T extends Comparable 。
限制
泛型的限制⼀般是由泛型擦除机制导致的。擦除为 Object 后⽆法进⾏类型判断。
内容
只能声明不能实例化 T 类型变量。
泛型参数不能是基本类型。因为基本类型不是 Object ⼦类,应该⽤基本类型对应的引⽤类型代替。
不能实例化泛型参数的数组。擦除后为 Object 后⽆法进⾏类型判断。
不能实例化泛型数组。
泛型⽆法使⽤ Instance of 和 getClass() 进⾏类型判断。
不能实现两个不同泛型参数的同⼀接⼝,擦除后多个⽗类的桥⽅法将冲突。
不能使⽤ static 修饰泛型变量。
应用
自定义接口通用返回结果 CommonResult<T> 通过参数 T 可根据具体的返回类型动态指定结果的数据类型。
定义 Excel 处理类 ExcelUtil<T> 用于动态指定 Excel 导出的数据类型。
构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。
擦除机制
类型擦除:编译期间,所有的泛型信息都会被擦掉。
Java 的泛型是伪泛型,因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是类型擦除 。
编译器会在编译期间会动态地将泛型 T 擦除为 Object 或将 T extends xxx 擦除为其限定类型 xxx 。
泛型本质上还是编译器的⾏为,为了保证引⼊泛型机制但不创建新的类型,减少虚拟机的运⾏开销,编译器通过擦除将泛型类转化为⼀般类。
桥⽅法( Bridge Method )
⽤于继承泛型类时保证多态,编译器⾃动⽣成。
通配符
通配符可以允许类型参数变化,⽤来解决泛型⽆法协变的问题。
⽆界通配符
⽆界通配符可以接收任何泛型类型数据,⽤于实现不依赖于具体类型参数的简单⽅法,可以捕获参数类型并交由泛型⽅法进⾏处理。
通配符 ?和通配符 T
T 可以⽤于声明变量或常量⽽ ? 不⾏。
T ⼀般⽤于声明泛型类或⽅法,通配符 ? ⼀般⽤于泛型⽅法的调⽤代码和形参。
T 在编译期会被擦除为限定类型或 Object ,通配符 ? ⽤于捕获具体类型。
边界通配符
上边界通配符 extends 可以实现泛型的向上转型即传⼊的类型实参必须是指定类型的⼦类型。
下边界通配符 super 与上边界通配符 extends 刚好相反,它可以实现泛型的向下转型即传⼊的类型实参必须是指定类型的⽗类型。
? extends xxx 和 ? super xxx
使⽤ ? extends xxx 声明的泛型参数只能调⽤ get() ⽅法返回 xxx 类型,调⽤ set() 报错。
使⽤ ? super xxx 声明的泛型参数只能调⽤ set() ⽅法接收 xxx 类型,调⽤ get() 报错。
T extends xxx 和 ? extends xxx
T extends xxx ⽤于定义泛型类和⽅法,擦除后为 xxx 类型。
? extends xxx ⽤于声明⽅法形参,接收 xxx 和其⼦类型。
Class<?> 和 Class
直接使⽤ Class 的话会有⼀个类型警告,使⽤ Class<?> 则没有,因为 Class 是⼀个泛型类,接收原⽣类型会产⽣警告。
反射(Reflection)
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
获取类模版
.class、Class.forName()、instance.getClass()、xxxClassLoader.loadClass()
优点
可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
缺点
让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。
应用
动态代理的实现依赖反射。
注解(Annotation)
JDK 5 新特性,用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
本质
一个继承了Annotation 的特殊接口。
解析方法
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理。
运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value、@Component)都是通过反射来进行处理的。
SPI vs API
SPI
SPI(Service Provider Interface)服务提供者的接口,专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或替换服务实现并不需要修改调用方。
实现本质上是通过反射完成的
META-INF/services/
优点
能够大大地提高接口设计的灵活性。
缺点
需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
当多个 ServiceLoader 同时 load 时,会有并发问题。
应用
Spring 框架、数据库加载驱动、日志接口SLF4J (Simple Logging Facade for Java)、以及 Dubbo 的扩展实现等等。
API(Application Programming Interface)
API(Application Programming Interface)应用程序接口,对外提供功能服务的一个接口。
序列化
数据结构或对象和二进制字节流互相转换过程。
序列化
将数据结构或对象转换成二进制字节流的过程。
反序列化
将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。
目的
通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
协议
常用协议
Hessian、Kryo、Protobuf、ProtoStuff,不推荐JDK自带(不支持跨语言调用、性能差、存在安全问题)。
像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
序列化协议对应于 TCP/IP 4 层模型的 表示层。
transient
对于不想进行序列化的变量,使用 transient 关键字修饰。
作用
阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
注意
transient 只能修饰变量,不能修饰类和方法。
transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。
static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。
应用
对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化。
将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化。
将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。
将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
语法糖(Syntactic sugar)
编程语言为了方便程序员开发程序而设计的一种特殊语法,compile() 中有一个步骤就是调用 desugar()。
常见语法糖
switch 支持 String 与枚举、泛型、自动拆装箱、变长参数、枚举、内部类。
条件编译、断言、数值字面量、for-each、try-with-resources、lambda表达式等。
代理模式
使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
作用
扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
静态代理
定义
实现和应用角度
对目标对象的每个方法的增强都是手动完成的,非常不灵活且麻烦。
JVM 角度
静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
步骤
定义一个接口及其实现类。
创建一个代理类同样实现这个接口。
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。
这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
动态代理
定义
实现和应用角度
不需要针对每个目标类都单独创建一个代理类,并且也不需要必须实现接口,可以直接代理实现类(CGLIB 动态代理机制)。
JVM角度
动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
应用
日常开发使用较少,但在框架中使用非常多。
JDK 动态代理
JDK 动态代理的目标对象必须要有接口实现,也就是说:委托类必须要继承接口。
核心
InvocationHandler 接口
在 invoke() 方法中可以自定义处理逻辑。
Proxy 类
Proxy.newProxyInstance()
主要用来生成一个代理对象。
代理对象在调用方法时,实际会调用实现 InvocationHandler 接口的类的 invoke()方法。
步骤
定义一个接口及其实现类。
自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中会调用原生方法(被代理类的方法)并自定义一些处理逻辑。
通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象,再调用接口方法。
CGLIB 动态代理
CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,允许在运行时对字节码进行修改和动态生成。
原理
CGLIB 通过继承方式实现代理。
核心
MethodInterceptor 接口
自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法。
Enhancer 类
通过 Enhancer 类来动态获取被代理类,当代理类调用方法时,实际调用的是 MethodInterceptor 中的 intercept 方法。
步骤
手动添加 CGLIB(开源项目)相关依赖。
定义一个类。
自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似。
通过 Enhancer 类的 create() 创建代理类,再调用接口方法。
应用
Spring 的 AOP 模块
如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
区别
JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。
效率上,JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
区别
灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。
另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
魔法类 Unsafe
依赖本地方法(Native Method)
native 关键字修饰,Java 中使用其他编程语言编写的方法。
初始化
类加载器、反射、VM参数
内存操作
在 Java 中不允许直接对内存进行操作,对象内存的分配和回收都是由 JVM 实现。但是在 Unsafe 中,提供接口可以直接进行内存操作。
释放
这种方式分配的内存属于堆外内存 ,是无法进行垃圾回收的,需要把这些内存当做一种资源去手动调用 freeMemory 方法进行释放,否则会产生内存泄漏。
通用的操作内存方式是在 try 中执行对内存的操作,最终在 finally 块中进行内存的释放。
内存屏障
CPU 为了防止代码进行重排序而提供的指令。
Java 8 中引入了一种锁的新机制:StampedLock,读写锁的一个改进版本。
loadFence()、storeFence()、fullFence()
对象操作
对象属性
put、get
对象实例化
常规对象实例化方式
new
非常规的实例化方式
allocateInstance
数组操作
arrayBaseOffset、arrayIndexScale
java.util.concurrent.atomic 包下的 AtomicIntegerArray
CAS 操作
比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。
三个操作数——内存位置、预期原值及新值。
JUC 包的并发工具类中大量地使用了 CAS 操作。
线程调度
park、unpark、monitorEnter、monitorExit、tryMonitorEnter
Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer (AQS)
Class 操作
类加载和静态变量的操作方法。
Lambda 表达式实现需要依赖 Unsafe 的 defineAnonymousClass 方法。
系统信息
addressSize、pageSize
0 条评论
下一页