总结
Java在官网也有相应的教程,去JDK官网也能了解到新版本JDK相关的特性,所以说直接浏览JDK官网是没有任何信息差的。(自己有充足时间的前提下)
《On Java》、《Effective Java》是必看的两本书,读书百遍,其义自见。读一遍也许理解不了什么。
Centos7安装JDK
安装步骤:
1、下载安装包,建议直接搜索jdk linux安装包,网上已经有人整理成帖子或者分享了网盘链接可以直接下载,省去了再去官网翻阅查找的麻烦。
2、解压缩
# 创建java文件夹备用
mkdir /usr/local/java
tar -zxvf jdk-8u11-linux-x64.tar.gz -C /usr/local/java
3、进入解压好的java目录,pwd一下bin目录路径
4、编辑/etc/profile
配置Java环境变量
vim /etc/profile
# 向文件中添加
export JAVA_HOME=/usr/local/java/jdk1.8.0_162
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin
5、配置完毕之后执行**source /etc/profile
**,环境变量即可生效。
1. Java概述
早期的Java是一种解释型语言。现在Java虚拟机使用了即时编译器,运行速度和C++相差无几。
编译型、解释型?(高级语言的分类)
编译型语言
定义:将高级语言源代码一次性的编译成能够被该平台执行的机器码并生成可执行程序。
特点:执行速度快、效率高;依靠编译器、跨平台性差。
包括:C、C++、Delphi、Pascal、Fortran…
解释型语言
定义:使用专门的解释器对源程序逐行解释成特定平台的机器码,逐行编译,解释执行。
特点:执行速度慢、效率低;依靠解释器、跨平台性好。
包括:Java、python、JavaScript…
因为编译型语言是一次性地编译成机器码,所以可以脱离开发环境独立运行,而且通过运行效率较高;但因为编译型语言的程序被编译成特定平台上的机器码,因为编译生成的可执行性程序通常无法移植到其他平台上运行;如果需要移植,则必须将源代码复制到特定平台上,针对特定平台进行修改,至少也需要采用特定平台上的编译器重新编译。
每次执行解释型语言的程序都需要进行一次编译,因此解释型语言的程序运行效率通常较低,而且不能脱离解释器独立运行。但解释型语言有一个优势:跨平台比较容易,只需提供特定平台的解释器即可,每个特定平台上的解释器负责将源程序解释成特定平台的机器指令即可。解释型语言可以方便地实现源程序级的移植,但这是以牺牲程序执行效率为代价的。
Java术语
Java安装
JDK JRE JVM
JDK包含了JRE和JVM。
JDK:(Java SE Development Kit)Java开发工具包,它提供了编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等。
JRE:( Java Runtime Environment) 、Java运行环境,用于解释执行Java的字节码文件。普通用户而只需要安装 JRE来运行 Java 程序。而程序开发者必须安装JDK来编译、调试程序。
JVM:(Java Virtual Machine),即Java虚拟机, 是JRE的一部分。它是Java实现跨平台最核心的部分,负责解释执行字节码文件,是可运行Java字节码文件的虚拟计算机。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。
当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码只面向JVM。不同平台的JVM都是不同的,但它们都提供了相同的接口。这就是Java的能够“一次编译,到处运行”的原因。
小结:
JDK用于开发,JRE用于运行应用程序,如果只是运行Java程序,那么只安装JRE就够了。
JDk包含JRE,JDK 和 JRE 中都包含 JVM。
JVM解释生成的与平台无关的字节码是Java实现跨平台的核心。
环境变量
path:系统运行可执行命令时搜索的路径列表。
作用:DOS下,根目录或者任意目录下运行任何可执行命令,必须配置可执行程序的(如.exe)的路径。
执行机制:path环境变量是先在当前目录找执行程序,如果没有,再到path指定目录中去寻找。
JAVA_HOME
:java的安装目录
安装
JDK
后,只要把/bin/java.exe
配置在系统PATH环境变量中,那么java
程序基本上能正常执行。基本上满足开发了开发要求。没有JAVA_HOME、CLASS_PATH也能正常执行。
为什么要配置JAVA_HOME?
(1)方便引用:比如,你JDK
安装在C:\ProgramFiles\Java\jdk1.7.0
目录里,则设置JAVA_HOME为该目录路径, 那么以后你要使用这个路径的时候, 只需输入%JAVA_HOME%
即可,避免每次引用都输入很长的路径串;
(2)归一原则:机器上如果JDK
版本发生了变化,java8
转到了java11
,安装路径发生了变化,这时候只需要改JAVA_HOME的值,那么相继引用JAVA_HOME的也会跟随变化。若没有配置JAVA_HOME,则需要手动修改PATH。
(3)第三方java
应用程序需要JAVA环境的支持,我觉得这也是配置JAVA_HOME最重要的一点。比如tomcat、groovy的运行需要用到JAVA_HOME。
CLASS_PATH:指定JRE
查找.class文件时的类路径
JAVA1.5
之后无需配置,JRE会自动搜索当前路径下的类文件。编译、运行时,系统可以自动加载dt.jar
和tools.jar
文件中的Java类。
如果指定了CLASS_PATH,会怎么样?
java
命令会严格按照CLASSPATH
变量中的路径来寻找class文件的,即使当前路径下有.class文件但是不在类路径下也不能执行。
小结:
JAVA_HOME开发中最好配置,提供第三方的环境。CLASSPATH
不用配置。PATH运行可执行命令都要必须配置。
查看win
java
环境变量的值 echo %JAVA_HOME%echo 输出命令
Java常用命令
# 查看java版本
java -version
# 编译java
javac Xxx.java
# 运行class文件 不带后缀
java Xxx
2. Java基础程序设计
1. 数据类型
Java是一种强类型语言。定义变量必须声明类型。
数据类型的分类不在介绍,请看思维导图
Java中int和long等类型的大小与平台无关(c++、c平台相关),这就解决了Java可移植、跨平台所带来的一些数据溢出等问题。
整型范围
类型 | 字节 |
---|---|
int | 4字节 |
long | 8字节 |
short | 2字节 |
byte | 1字节 |
浮点型
类型 | 字节 |
---|---|
float | 4字节 |
double | 8字节 |
字符和布尔类型
类型 | 字节 |
---|---|
char | 2 |
boolean | 1 |
注:float|Float、double|Double,Long加后缀
float声明的变量数值类型必须要有F或f,默认没有后缀F的浮点数值默认为double类型,如果 float f = 3.14
没有指定,idea出现类型转化错误(double->float).
(Long)长整型引用类型声明需要添加后缀。
true false和null不属于关键字,他们是一个单独标识类型,不能直接使用。
2. 类型转换
数值类型之间的隐式转换:默认都会向上转型,虚线箭头代表可能有精度损失的转化。
注意,这里将char也归入到了数值型之间的类型转化,char类型本质上是以ASCII码的形式存储的。
隐式只能向上转,可以跨多个维度。
char a = 'A';// char a = 65; 是一样的效果,char类型本质上是以ASCII码的形式存储的
double d = a;
System.out.println(d);//65.0
强制类型转换
int i = (int) 3.14;
隐式类型转化
数值运算过程中的类型转化问题:
-
所有的byte,short,char型的值将被提升为int型;
-
final修饰的变量运算会被JVM自动优化为字面量(常量)
-
如果有一个操作数是long型,计算结果是long型;
-
如果有一个操作数是float型,计算结果是float型;
-
如果有一个操作数是double型,计算结果是double型;
1、运算过程中的向上转型
float d = 3.1 + 2.6;//error,运算结果为double类型
double d = 3.1 + 2.6;
2、复合赋值运算符自动类型强转
byte a = 2;
byte b = 3;
a = a + b; //error
byte类型的变量在做运算时被会转换为int类型的值,int赋值给结果类型为byte的变量发生错误,同理,如果int和float混合运算,会向精度高的类型转化,所以也会发生错误。但是
如果采用复合赋值运算符则将会自动类型强转
@Test
public void testAdd() {
byte a = 2;
byte b = 3;
// a = a + b; //error
a += b;
System.out.println(a);//5
float x = 1.2f;
int y = 3;
// y = y + x;
y += x;
System.out.println(y);//4
}
3、final修饰的变量运算会被JVM自动优化为字面量(常量)
final byte a = 10, b = 20;
byte c;
// a + b被编译为字面量相当于30,在编译器就已经确定了
c = a + b;
3. Java运算符
Java运算符的分类:算数运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、条件运算符
算数运算符
分类:+ 、-、*、/、%、自增(++)、自减(--)
Java中的取模对浮点型也适用,取模结果的符号取决于第一个运算数
进行算数运算的操作数若类型不一致,则会发生类型转化
关系运算符
分类:>、< 、==、>= 、<=、!=
关系运算符是一个二元运算符,也就是说必须要有两个操作数,运算结果是一个逻辑值(boolean)
Java中"=="和"!="适用于任何数据类型。可以判断数值是否相等,还可以判断对象或数组的实例是否相等
逻辑运算符
分类
逻辑与( && ) :全真则真
逻辑或( || ):一真则真
逻辑非( ! ):真则假,假则真
逻辑与(&)逻辑非 (!)
&&和||运算符属于二元运算符,逻辑非属于一元运算符,并且Java中 && 和 || 存在"短路"效应
&&和&、||和|相同点是运算结果一样,不同点是&和|没有短路效应即当左边条件不成立时也会执行右边的判断
位运算符
概念:位算符主要用来对二进制位进行操作,其操作数和运算结果都是整型值
分类
位与(&):同1则1
位或( | ):有1则1
位异或(^):位相异为1,相同为0
位非(取反) ~:单目运算,0变1,1变0
左移(<<):高位舍弃,低位补零(正负数都一样)
右移(>>):低位舍弃,高位补符号位(正数补零,负数补一)
无符号右移(>>>):忽略符号位,高位通补0
注:计算机是以数字的补码进行运算
赋值运算符
=、各种运算简化赋值(+=、-=...)
条件运算符
三目运算符:expression ? (true->exp1) : (false->exp2)
运算符优先级如下图,级别由高到低。
类别 | 操作符 | 关联性 |
---|---|---|
后缀 | () [] . (点操作符) | 左到右 |
一元 | expr++ expr– | 从左到右 |
一元 | ++expr --expr + - ~ ! | 从右到左 |
乘性 | * /% | 左到右 |
加性 | + - | 左到右 |
移位 | >> >>> << | 左到右 |
关系 | > >= < <= | 左到右 |
相等 | == != | 左到右 |
按位与 | & | 左到右 |
按位异或 | ^ | 左到右 |
按位或 | | | 左到右 |
逻辑与 | && | 左到右 |
逻辑或 | | | | 左到右 |
条件 | ?: | 从右到左 |
赋值 | = + = - = * = / =%= >> = << =&= ^ = | = | 从右到左 |
逗号 | , | 左到右 |
3. 字符串
知识点:final关键字 不可变对象 String常量池 编码
1. final关键字
*final关键字的用法:
- final修饰类,表示该类不可以被继承,俗称断子绝孙类
- final修饰方法,子类不可重写该方法,final修饰的方法可以被重载,但不能被重写。
- final修饰基本数据类型变量,表示该变量为常量,变量值不能被修改
- final修饰引用类型变量,表示该引用在初始化指向某对象后,这个引用不能在指向其他对象,但该引用的对象的状态可以改变。
疑问点:引用的对象的状态可以改变?
final int a = 10;
a = 20; //error final 修饰基本类型,值不可变
final String str = "hello world!";
str = "java"; // error final 修饰引用类型 引用的对象地址不能发生变化
final StringBuilder builder = new StringBuilder("hello world");
builder.append("!");// final 修饰引用类型,对象的状态,这里就是指的builder构造的字符串可以发生变化
builder = new StringBuilder();// error 但是builder引用的地址不能在变
final修饰时,要能够区分是引用不可变,还是内部状态不可变!
final作为对象成员存在时,必须初始化;但是,如果不初始化,也可以在类的构造函数中初始
2. 不可变对象
不可变对象(Immutable Object):对象一旦被创建后,对象所有的状态及属性在其生命周期内不会发生任何变化。
如果一个对象在创建之后就不能再改变它的状态,那么这个对象是不可变的(Immutable)。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型变量的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。
如何创建不可变对象
1)所有属性私有化,并用final标记(非必须)
2)不提供修改原有对象状态的方法
- 去除setter方法
- 如果提供修改方法,则需要新创建对象,并在新创建的对象上进行修改
3)最好不允许类被继承
4)通过构造器初始化所有成员变量,引用类型的变量必须进行深拷贝(deep copy)
3. String的不可变性
String对象一旦被创建并初始化后,这个对象的值就不会发生变化。String类中的所有方法并不是改变String对象本身,相关的任何change操作都是重新创建一个新的String对象。
为什么String类是不可变的?
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
........
}
String底层存储字符串就是依靠private final char value[];
一个字符数组,并且String没有提供修改value值的方法,sub、replace不是吗?这些都在新生成的String实例上操作的,并不是改变对象原有的状态,这正好对应上面如何创建不可变对象的原则。正因为如此String对象创建后,就不能修改对象中存储的字符串内容,所以String类型是不可变的(immutable)。
String的不可变是否是真的不可变?
这里仅仅记录一下,网上也有很多这样的帖子,留个印象。
private final char value[];
String底层就是用value数组来存储的字符串,final修饰,数组引用不可变化,但是数组中的值可以变化,通过反射可以改变数组引用内部变量的值。
String str = "hello world_";
Field field = str.getClass().getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[str.length() - 1] = '!';
System.out.println(value);//hello world!
如果利用反射,那就根本就不存在什么可变不可变的了。
- TODO 字面量 编译期 运行期 串的存在 堆 栈
4. 字符串常量池
字符串的分配和其他对象分配一样,需要消耗高昂的时间和空间,而且字符串我们使用的非常多,JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。
必知一:当我们创建字符串常量时,JVM首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串。
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
String a = "hello";
String b = "hello";
String c = new String("hello");
上述变量的内存图示:
必知二:字面量"+“拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;而字符串引用的”+"拼接运算是在运行时进行的,新创建的字符串放在堆中。
所以String相关的疑问点就迎刃而解了!
String s = " world";
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);// true
String s3 = new String("hello");
System.out.println(s1 == s3);//false
String s4 = "hello world";
System.out.println(s4 == ("hello" + " world"));//true
String s5 = s1 + s2;
System.out.println(s4 == s5);//false
5. 字符集和编码规则
**字符集:**多个字符的集合。例如GB2312是中国国家标准的简体中文字符集,GB2312收录简化汉字(6763个)及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母、汉语拼音符号、汉语注音字母,共 7445 个图形字符。
**字符编码:**把字符集中的字符编码为(映射)指定集合中的某一对象(例如:比特模式、自然数序列、电脉冲),以便文本在计算机中存储和通过通信网络的传递。
字符集和字符编码的关系 :
字符集是书写系统字母与符号的集合,而字符编码则是将字符映射为一特定的字节或字节序列,是一种规则。通常特定的字符集采用特定的编码方式(即一种字符集对应一种字符编码(例如:ASCII、IOS-8859-1、GB2312、GBK,都是即表示了字符集又表示了对应的字符编码,但Unicode不是,它采用现代的模型)),因此基本上可以将两者视为同义词。
字符集:为每一个「字符」分配一个唯一的 ID(学名为码位 / 码点 / Code Point)
编码规则:将「码位」转换为字节序列的规则(编码/解码 可以理解为 加密/解密 的过程)
4 对象 引用
1. 何为对象?何为对象的引用?
每种编程语言都有自己的数据处理方式。有些时候,程序员必须注意将要处理的数据是什么类型。你是直接操纵元素,还是用某种基于特殊语法的间接表示(例如C/C++里的指针)来操作对象。所有这些在 Java 里都得到了简化,一切都被视为对象。因此,我们可采用一种统一的语法。尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“引用”(reference)。
—《Java编程思想》
也就是说,在Java中我们操作对象都是通过引用来间接操作对象的。一个引用可以指向多个对象,一个对象也可以被多个引用所指。(相当于C/C++的指针,但又有所差别)
Object o; // 栈空间创建引用变量,此时o默认为null
o = new Object();// 堆空间实例化一个对象,栈空间的引用变量o指向了堆空间中该对象的地址。
Object obj = new Object();// 堆空间实例化对象后,引用obj指向了该地址
Object o = new Object();
Object o1 = o;
Object o2 = o;
System.out.println(o == o1);// true
System.out.println(o2 == o1);// true
// 一个对象可以被多个 引用变量(对象引用) 同时引用
o1 = new Object();
// 一个引用可以指向不同的对象,但同一时刻只能指向一个
System.out.println(o == o1);// false
如下表达式:
A a1 = new A();
它代表A是类,a1是引用,a1不是对象,new A()才是对象,a1引用指向new A()这个对象。在JAVA里,“=”不能被看成是一个赋值语句,它不是在把一个对象赋给另外一个对象,它的执行过程实质上是将右边对象的地址传给了左边的引用,使得左边的引用指向了右边的对象。JAVA表面上看起来没有指针,但它的引用其实质就是一个指针,引用里面存放的并不是对象,而是该对象的地址,使得该引用指向了对象。在JAVA里,“=”语句不应该被翻译成赋值语句,因为它所执行的确实不是一个赋值的过程,而是一个传地址的过程,被译成赋值语句会造成很多误解,译得不准确。
再如:
A a2;
它代表A是类,a2是引用,a2不是对象,a2所指向的对象为空null;再如:
a2 = a1;
它代表,a2是引用,a1也是引用,a1所指向的对象的地址传给了a2(传址),使得a2和a1指向了同一对象。综上所述,可以简单的记为,在初始化时,“=”语句左边的是引用,右边new出来的是对象。
在后面的左右都是引用的“=”语句时,左右的引用同时指向了右边引用所指向的对象。再所谓实例,其实就是对象的同义词。
小结:
对象,堆中的空间;对象的引用保留了对象在堆中的地址,按C/C++指针理解的话,就是指针(引用)指向了对象。总的来说,我觉得引用本质上就是指针,只不过它和C++指针有所不同,他淡化了C++一些难以理解的抽象概念和用法,比如说在C++中指针可以自增减,Java没有这样的操作,开发者也不用担心自增减所带来一些异常等问题。
一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new操作符的返回值也是一个引用。
2. 引用相等和对象状态(内容)相等
(1)对于==,比较的是值是否相等
如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;
如果作用于引用类型的变量,则比较的是所指向的对象的地址
(2)对于equals方法,注意:equals方法不能作用于基本数据类型的变量,equals继承Object类,比较的是是否是同一个对象
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
总结:
通过以上例子可以说明,如果一个类没有自己定义equals()方法,它默认的equals()方法(从Object类继承的)就是使用“”运算符,也是在比较两个变量指向的对象是否是同一对象,此时使用equals()方法和使用“”运算符会得到同样的结果。若比较的是两个独立的对象,则总返回false。如果编写的类希望能够比较该类创建的两个实例对象的内容是否相同,那么必须覆盖equals()方法,由开发人员自己编写代码来决定在什么情况下即可认为两个对象的内容是相同的。
3. 值传递和引用传递
概念 方法参数 Java值传递
错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。
错误理解二:Java是引用传递。
错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。
**值调用:**方法接受的是调用者提供的值
**按引用调用:**方法接受的是调用者提供的变量地址
Java程序设计语言总是采用按值调用(call by value),方法得到的是所有参数值的拷贝。
public class CallByTest {
public static void main(String[] args) {
int a = 10;
int b = 20;
change(a, b);
System.out.println(a + " " + b);// 10 20
StringBuilder builder1 = new StringBuilder();
StringBuilder builder2 = new StringBuilder();
builder1.append("hello");
builder2.append("world!");
change(builder1); // hello,world!
swap(builder1, builder2);// hello,world! world!
}
private static void change(StringBuilder builder) {
builder.append(",world!");
}
/**
* Java对象采用的是按引用调用,那么该方法就能够实现交换数据,但是方法并没有改变builder1和builder2对象 * 的引用。说明swap方法参数只是两个对象引用的拷贝,方法交换的是拷贝,当方法退栈之后,拷贝就会被丢弃,对原 * 有对象引用没有影响。
*/
private static void swap(StringBuilder b1, StringBuilder b2) {
StringBuilder temp = b1;
b1 = b2;
b2 = b1;
}
private static void change(int a, int b) {
int temp = a;
a = b;
b =temp;
}
}
总结Java对象参数的使用情况
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用一个新的对象
4. hashCode和equals方法
重写equals方法的场景:当需要判断两个对象的引用内容相等时,需要重写equals方法
为什么重写equals方法通常有必要重写hashCode方法
重写hashCode的要求
生活中我们用身份证来标识一个人,身份证号码具有唯一性,如果两个人的身份证号一样,那么就说明这两个人是一个人。所以在程序中我们重写equals方法,以id作为判断两个对象是否相等的依据。
public class Person {
String id;// 身份ID,唯一标识
String name;
int age;
...
@Override
public boolean equals(Object otherObject) {
if (otherObject == this) {// 自身性
return true;
}
if (otherObject == null) {
return false;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) otherObject;
// 身份ID相等则为同一个人 调用Objects.equals()防止NullPointException
return Objects.equals(id, other.id) && other.id.equals(this.id);
}
}
测试
Person p1 = new Person("001", "小明", 18);
Person p2 = new Person("001", "小明明", 18);
System.out.println(p1 == p2);// false 引用肯定不同
System.out.println(p1.equals(p2));//true
==============Set集合测试============================
HashSet<Person> peoples = new HashSet<>();
peoples.add(p1);
peoples.add(p1);// Set集合特性:相同的引用,只会保留一个
peoples.add(p2);// p2 虽然和 p1是同一个人,但因为没有重写hashCode方法,所以还是会存入HashSet中
System.out.println(peoples);//[corejava1.Person@677327b6, corejava1.Person@1540e19d]
程序达到了我们的预期,p1和p2姓名不同但是ID一样(同一个人可能之后改名了),这说明就是同一个人。
p1.equals(p2)
并不会调用hashCode
方法,所以单单用equals方法就可以完成我们的实际需求为什么还要hashCode
?正如Set集合测试那样,使用Set集合存放这两个对象(实质应该是同一个人),Set集合特性不会保留重复元素,但是实际测试结果,Set集合将对象都放入了容器。
重写hashCode()
,再次测试就不会出现上述结果了。
@Override
public int hashCode() {
int result = 17;
result = result * 31 + id.hashCode();
return result;
}
重写hashCode
的要求:
-
初始化一个整型变量,为此变量赋予一个非零的常数值,通常为
int result = 17
-
对equals方法中用于比较的所有域的属性进行计算,不能包含equals方法中没有的字段,否则会导致相等的对象可能会有不同的哈希值。
(1) 如果是boolean值,则计算f ? 1:0
(2) 如果是byte\char\short\int,则计算(int)f
(3) 如果是long值,则计算(int)(f ^ (f >>> 32))
(4) 如果是float值,则计算Float.floatToIntBits(f)
(5) 如果是double值,则计算Double.doubleToLongBits(f),然后返回的结果是long,再用规则(3)去处理long,得到int
(6) 如果是对象应用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。 否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode 值为0
(7) 如果是数组,那么需要为每个元素当做单独的域来处理。如果你使用的是1.5及以上版本的JDK,那么没必要自己去 重新遍历一遍数组,java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上,
@Override public int hashCode() { int result = 17; result = 31 * result + mInt; result = 31 * result + (mBoolean ? 1 : 0); result = 31 * result + Float.floatToIntBits(mFloat); result = 31 * result + (int)(mLong ^ (mLong >>> 32)); long mDoubleTemp = Double.doubleToLongBits(mDouble); result =31 * result + (int)(mDoubleTemp ^ (mDoubleTemp >>> 32)); result = 31 * result + (mString == null ? 0 : mMsgContain.hashCode()); result = 31 * result + (mObj == null ? 0 : mObj.hashCode()); return result; }
为什么是计算中会有17和31,请参考**正确重写hashCode方法、通用的Java hashCode重写方案**
小结
-
equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。
-
hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
因为散列表肯定用到equals,所以说对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equal()去再对比了),如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性。
额外扩展
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。
虽然,每个Java类都包含hashCode() 函数。但是,仅仅当创建并某个“类的散列表,该类的hashCode() 才有用(作用是:确定该类的每一个对象在散列表中的位置;其它情况下(例如,创建类的单个对象,或者创建类的对象数组等等),类的hashCode() 没有作用。
上面的散列表,指的是:Java集合中本质是散列表的类,如HashMap,Hashtable,HashSet。
也就是说:hashCode() **在散列表中才有用,在其它情况下没用。**在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
5. 栈、堆、方法区
public class Person {
String name;
int age;
public void show() {
System.out.println(name + " age:" + age);
}
}
class PersonTest {
public static void main(String[] args) {
Person person = new Person();
person.name = "Stronger";
person.age = 20;
person.show();
}
}
- TODO未完结
6. 浅拷贝和深拷贝
1. 浅拷贝(Shallow Copy)
浅拷贝,按位拷贝对象,会创建新对象,这个对象的属性值有着和原始对象的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果是引用类型,拷贝的是引用类型的内存地址,因此如果其中一个对象状态发生改变,就会影响到另一个对象。(值拷贝[变量值引用值])
2. 深拷贝(Deep Copy)
深拷贝,拷贝所有属性,如果属性为引用类型,则会为该属性新开辟一块独立的内存空间,拷贝属性指向的动态分配的内存。(内存拷贝)
总结:两者的差异之处,浅拷贝,值拷贝;深拷贝,内存拷贝。
如何实现拷贝?
要拷贝的类必须实现Cloneable接口,如果要在外部类调用则需要,**“修改”**clone()方法
Object中的clone函数,方法修饰符为protected
,说明只能在实现Cloneable接口的类(子类)内部调用,默认都继承自超类,如果子类在没有实现Cloneable接口的前提下调用会抛出exception at run time.
;若类实现了Cloneable接口,并重写了方法,默认为protected,要想在外部调用,必须将修饰符改为public。
protected native Object clone() throws CloneNotSupportedException;
示例
class Address implements Cloneable{
private long postId;
private String addrInfo;
...
/**
* 默认方法的修饰符为protected,为了在外部能够调用
* 修改方法修饰符
* @return
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
如何实现深拷贝?
在一个类中的属性成员如果是一个引用类型的变量,浅拷贝默认是拷贝的引用的对象地址,更改拷贝后对象的状态会影响之前对象的状态,这时候就要使用深拷贝,当拷贝引用类型的属性时,同时拷贝动态的内存空间。
示例:
public class Person implements Cloneable{
private String id;// 身份ID,唯一标识
private String name;
private int age;
private Address address;
...
/**
* 默认方法的修饰符为protected,为了在外部能够调用
* 修改方法修饰符
* @return
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
Address address = person.getAddress();
Address addrDeepCopy = (Address) address.clone();
person.setAddress(addrDeepCopy);
return person;
}
}
class Address implements Cloneable{
private long postId;
private String addrInfo;
...
/**
* 默认方法的修饰符为protected,为了在外部能够调用
* 修改方法修饰符
* @return
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
7. main方法传参
java 类 param1, parm2, param3
3. 对象与类
3.1 类
类:具体事物的抽象。
类是描述了一组有相同特性(属性)和相同行为(方法)的一组对象的集合。
类之间的关系:
-
依赖(uses-a)—如果一个类的方法操纵另一个类的对象,我们就睡一个类依赖于另一个类。
-
聚合(has-a)—类A的对象包含类B的对象
-
继承(is-a)—如果类A继承自类B,类A不但包括从类B继承的方法,还会拥有一些额外的功能。
表示类之间关系的UML符号
**注:**开发中应尽可能地将相互依赖的类减至最少。如果类A不知道类B的存在,它就不会关心B的任何改变(B的任何改变不会对类A产生影响)。用软件工程的术语来说,就是让类之间的耦合度最小。
在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。
3.1.1 类的定义
格式
[修饰符] [static] [final] [abstract] [strictfp] class 类名 [extends 父类名] [implements 接口名] {
...
}
3.1.2 修饰符
Java中的修饰符包括访问修饰符和非访问修饰符
修饰符上面指的就是访问权限修饰符了,包括
- public:对所有类课件。使用对象:类、接口、变量、方法
- protected:对同一包内的类和所有子类可见。适用对象:变量、方法。注:不能修饰类(外部类)
- private:类内可见。适用对象:变量、方法。注:不能修饰类(外部类)
- 默认(缺省值):什么也不写,同一包内可见。使用对象:类、接口、变量、方法
访问权限:public > protected > default > private
非访问修饰符
- static 修饰符,用来修饰类方法和类变量。(不能修饰外部类,内部类可以)
- final 修饰符,用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。
- abstract 修饰符,用来创建抽象类和抽象方法。
- synchronized 和 volatile 修饰符,主要用于线程的编程。
3.2 对象
对象:类的实例。
类是概念模型,定义对象的所有特性和所需的操作,对象是真实模型,是具体的一个实体。类是构造对象的模板或蓝图。
对象的三个主要特性:
- 对象的行为(behavior)—可以对对象施加哪些操作,或可以对对象施加哪些方法
- 对象的状态(state)—当施加那些方法时,对象如何响应
- 对象标识(identity)—如何辨别具有相同行为与状态的不同对象
面向对象的三大核心特性:封装、继承、多态
3.2.1 对象构造
实例化对象的方法:
-
通过new 类即调用构造函数实例化类
-
调用 java.lang.Class 或者java.lang.reflect.Constuctor 类的 newlnstance()实例方法(默认通过无参构造函数构造)
java.lang.Class Class 类对象名称 = java.lang.Class.forName(要实例化的类全称); 类名 对象名 = (类名)Class类对象名称.newInstance();
-
调用对象的clone()方法(必须实现Cloneable接口)
-
序列化
# nowcoder:
构造方法不能被static、final、synchronized、abstract、native修饰,但可以被public、private、protected修饰。
识别合法的构造方法;
1 构造方法可以被重载,一个构造方法可以通过this关键字调用另一个构造方法,this语句必须位于构造方法的第一行;
2 当一个类中没有定义任何构造方法,Java将自动提供一个缺省构造方法;
3 子类通过super关键字调用父类的一个构造方法;
4 当子类的某个构造方法没有通过super关键字调用父类的构造方法,通过这个构造方法创建子类对象时,会自动先调用父类的缺省构造方法
5 构造方法不能被static、final、synchronized、abstract、native修饰,但可以被public、private、protected修饰;
6 构造方法不是类的成员方法;
7 构造方法不能被继承。
3.2.2 重载
重名不重参,返回值无关
构造函数重载:一个类有多个构造器,针对不同的构造器提供的初始化构造条件(参数类型或者个数)来区别调用哪一个。
成员方法重载:定义和构造函数的意思一致。方法名相同,参数类型或者个数不同。
Java允许重载任何方法,而不只是构造器方法。因此要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)
例如String类中indexOf的方法签名
indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)
注:**返回类型不是方法签名的一部分。**不能有两个名字相同、参数类型也相同却返回类型不同的方法。
**默认域初始化:**如果在构造器中没有显式地给域赋初值,那么就会被自动地赋为默认值,数值类型为0,布尔值为false、对象引用为null。
默认构造器相关:**当类没有提供任何构造器的时候,系统才会提供一个默认的构造器。默认的构造方法不包含任何参数,并且方法体为空。**如果在编写类的时候,给出了一个或多个构造方法,则系统不会再提供默认构造方法。
3.3 静态域与静态方法
静态域:如果将域定义为static,每个类中只有一个这样的域。而一个对象对于所有的实例域却都有自己的一份拷贝。
比如有1000个Employee对象,则有1000个实例域id。但是,只有一个静态域nextId。即使没有一个Employee对象,静态域nextId也存在。它属于类,而不属于任何一个独立的对象。
静态方法:静态方法是一种不能面向对象实施操作的方法。(不依赖于对象)
不能调用类中非静态属性,可以调用静态属性。
静态代码块&构造函数执行顺序:static->constructor,若类之间存在继承,执行顺序为:父类static->子类static->父类构造->子类构造(静态代码块优先执行)
3.3 静态块和构造块
静态块
- static修饰,所属类,主要用来为类的属性进行初始化,随着类的加载而执行,且只能执行一次。
- 静态代码优先于非静态代码执行,如果类中存在多个静态块,执行顺序与定义顺序相关。
构造块
-
类中被{…}所包裹的程序块,所属对象,主要用来为对象初进行初始化操作(很少),new一次执行一次。
-
执行顺序优先于构造函数,多个构造块执行顺序和定义顺序呢相关。
构造块和构造函数的区别:
构造代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。
3.4 继承
继承:继承已存在的类就是复用(继承——这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。
继承的核心:代码复用
继承是一个“是不是”的关系,而接口实现则是“有没有“的关系
3.4.1 this&super
this用途
1)引用引式参数(调用实例方法,一般不显示使用this)
2)调用该类其他的构造器
super用途
1)调用超类的方法
2)调用超类的构造器
注意点:
1)this、super在调用本类或者超类的构造器的时候,调用语句只能出现在第一行,且this和super在构造器中只能出现一个(不共存)
2)调用超类的方法必须有super参数super.xxx(...)
,否则执行的是this(当前类)所调用的方法this.xxx(...)
3)this是一个对象的引用,super不是
3.4.2重载和重写
**方法重载:**多个同名函数同时存在,具有不同的参数个数或者类型,调用方法根据类型个数和次序确定调用哪一个。
**方法重写:**子类修改了默认从父类中继承的方法。
重载和重写的区别(一图概括了重写和重载时的注意点)
作用位置 | 修饰符 | 返回值 | 方法名 | 参数 | 抛出异常(类型) | |
---|---|---|---|---|---|---|
重载(overload) | 同类中 | 无关 | 无关 | 相同 | 不同 | 无关 |
重写(override) | 子类和父类 | 大于等于 | 小于等于 | 相同 | 相同 | 小于等于 |
重写函数签名(方法名、参数类型、次序)必须相同,子类返回值类型必须小于等于父类返回值类型,修饰符权限父类小于等于子类。
方法重载无关返回值和修饰符。
3.4.3 多态
1)什么是多态?
同一个行为对于不同事物具有多种不同的表现形式或形态叫做多态。
不同对象对同一消息做出不同的反应或行为。
例:就像上课铃响了,上体育课的学生跑到操场上站好,上语文课的学生在教室里坐好一样。
2)多态的分类
编译时多态:根据方法参数类型、个数和次序方法来确定调用的哪一个方法,对应的就是方法重载。
运行时多态:程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定。主要是靠方法重写实现。
3)Java实现多态的三个必要条件:继承、重写和向上转型
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。
分析多态问题的原则
- 向上转型是自动的,不需要强转。
- 向下转型要强转。必须要让父类知道它要转换成哪个具体子类。
- 父类引用子类对象,子类重写了父类的方法,调用父类的方法,实际调用的是子类重写了的父类的该方法。
总结:三定义两方法,父类定义子类构建、接口定义实现类构建和抽象类定义实体类构建。两方法,方法重载和方法重写。
代码示例:
public abstract class Person {
private String name;
public abstract void getDescription();
public Person() {}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Employee extends Person{
static {
System.out.println("Employee static ...");
}
private String name;
private double salary;
private LocalDate hireDay;
public Employee() {
System.out.println("Employee()...");
}
public Employee(String name, double salary, int year, int month, int day) {
super(name);
System.out.println("Employee constructor...");
this.name = name;
this.salary = salary;
this.hireDay = LocalDate.of(year, month, day);
}
// getter ...
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
@Override
public void getDescription() {
System.out.println("Employee getDescription...");
}
}
public class Manager extends Employee {
static {
// 静态代码块优先,父类static->子类static->父constructor—>子constructor
System.out.println("Manger static ...");
}
private double bonus;
public Manager() {
// super();默认存在于每个构造函数的第一行,前提是父类必须存在一个无参构造
// 或者显示调用super有参构造
System.out.println("Manger()...");
}
public Manager(String name, double salary, int year, int month, int day) {
// Manger类假如没有无参构造,并且Employee也没有无参构造,
// 那么必须必须显示的调用父类的某个构造初始化父类,否则编译不通过(如下)
super(name, salary, year, month, day);
bonus = 0;
}
public double getSalary() {
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
}
测试:
public void testInherit() {
Manager boss = new Manager("Stronger", 8000, 2020, 11, 11);
boss.setBonus(2000);
Employee[] employees = new Employee[3];
employees[0] = boss; // 1. 父类可以引用子类的对象
// employees[0].setBonus(2000); error 现在类型父为Employee,子类的方法不可见,没有setBonus方法
employees[1] = new Employee("Gang", 5000, 2020, 11, 10);
employees[2] = new Employee("Long", 6000, 2020, 11, 9);
for (Employee e : employees) {
// 2. 动态绑定(dynamic binding): employee[0]调用的是Manger里的方法
System.out.println(e.getName() + " salary:" + e.getSalary());
}
// 3. Java中,子类数组的引用可以转换成超类数组的引用;但是,不能将
// 一个超类的引用赋给子类变量。
Manager[] managers = new Manager[10];
Employee[] staff = managers;
//staff[0] = new Employee("Long", 6000, 2020, 11, 9); 编译通过,但是运行不通过抛出
// java.lang.ArrayStoreException
// 4.强制类型转化 对象强转的一个原因是要适用对象的全部功能,前提是强转后的对象类型是特定类的实例
// employees[0]强转后必须是Manger类型
Manager m = (Manager) employees[0];
System.out.println(m.getSalary());
// 若强转后的对象不是Manger类型
// m = (Manager) employees[1]; 运行抛出java.lang.ClassCastException
// 所以说我们可以通过 instanceof运算符判断某个对象是否为某类的实例
if (employees[1] instanceof Manager) {
m = (Manager) employees[1];
}
// 5. 抽象类
// new Person(); ERROR 抽象类不可以实例化
Person p = new Employee();// 抽象类的变量只能引用非抽象子类的对象
p.getDescription();
}
3.4.4 Object类
Object是所有类的超类,可以使用Object类型的变量引用任何类型的对象。Object类型的变量只能用于作为各种值的通用持有者。要想对其中的内容进行具体的操作,还需要清除对象的原始类型,并进行相应的类型转换。
在Java中,只有基本类型不是对象,例如,数值、字符和布尔类型的值都不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
3.4.5 自动装箱和自动拆箱
装箱:将基本数据类型转化为包装类型。
拆箱:将包装类型转化为基本数据类型。
// 自动装箱
Integer autoBoxing = 99;
// 反编译之后其实执行了 Integer.valueOf(99);
// 自动拆箱
int unboxing = autoBoxing;
//反编译:autoBoxing.intValue();
反编译:javap -c 类名
支持自动拆装箱的类型:
必知必会
Integer相关源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
示例
@Test
public void testWrapper() {
/*
1. 包装类型比较
首先a、b、c、d变量进行了自动装箱,将基本类型转化为包装类型
"=="比较的是引用的对象是否为同一个
a == b为true,[-128,127]从缓冲中取,得到的是同一个对象的引用
c == d为false,number<-128 && number> 127则是会实例化对象new Integer(number);
*/
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
System.out.println(a == b); // true
System.out.println(c == d); // false
/*
2. 基本类型和包装类型进行 == 、+、-、*、/运算或者
当操作符一侧为封装类型的表达式(包装类型),
封装类自动拆箱为基本类型,所以比较和运算的都是基本类型。
*/
int a1 = 100;
int c1 = 300;
System.out.println(a == a1); // true
System.out.println((a+c) == c1); // true
/*
3. 包装类型在拆包的时候可能会引发空指针异常
*/
Integer d1 = null;
d1.intValue();
}
总结:
1)基本类型与包装类型进行比较的时候,包装类型会自动拆箱,按值比较。
2)包装类型与包装类型进行比较时
2.1)new实例化的包装类型比较永远为false。
2.2)非new的,如果范围在[-128,127]直接,比较按值比较,否则符合(2.1)这种情况。
public static void main(String[] args) {
// 基本类型与包装类型的比较
int a1 = 127;
Integer a2 = 127;
Integer a3 = new Integer(127);
System.out.println(a1 == a2);
System.out.println(a1 == a3);
int a4 = 128;
Integer a5 = 128;
Integer a6 = new Integer(128);
System.out.println(a4 == a5);//true
System.out.println(a4 == a6);//true
// 包装类型的比价
//1.非new产生,反编译实际是调用了Integer.valueOf()
// 方法来生成一个Integer类型的对象。而Integer.valueOf()
// 方法中当其范围属于[-128,127]之间则从缓冲区中取,否则
// new一个对象。
Integer b1 = 127;
Integer b2 = 127;
Integer b3 = 128;
Integer b4 = 128;
System.out.println(b1 == b2);//true
System.out.println(b3 == b4);//false
//2.new比较,不会相等,除非自身比较
Integer c1 = new Integer(127);
Integer c2 = new Integer(127);
Integer c3 = new Integer(128);
Integer c4 = new Integer(128);
System.out.println(c1 == c2);//false
System.out.println(c3 == c4);//false
}
3.5 枚举类
枚举类型本质上是类类型,枚举类是一种特殊的类。
应用场景:定义常数集合
public enum Weekday {
MON, TUE, WED, THU, FRI, SAT,SUN;
}
枚举类基础知识
-
枚举enum是一个特殊的class,相当于final修饰,不能被继承。
-
构造函数强制私有化,所以说不可以在外部实例化对象,有一个默认空参构造。
-
当使用枚举类的时候,只需通过类名.属性名访问,枚举类对象自动调用构造函数实例化,类中所有变量全部实例化,且只实例化一次。
-
枚举类的所有实例必须在枚举类的第一行显示列出,否则这个枚举类永远都不能产生实例
-
获取枚举常量的方法
类名.常量名
类名.valueOf(常量名 String)返回一个Enum实体
自定义枚举类
public enum EnumEntity {
FIRST,SECOND,THIRD
}
反编译之后的代码,通过浏览编译后的代码,也就能理解为什么enum不能继承,为什么可以直接"类名.常量名"引用…
public final class EnumEntity extends java.lang.Enum<EnumEntity> {
public static final EnumEntity FIRST;
public static final EnumEntity SECOND;
public static final EnumEntity THIRD;
public static EnumEntity[] values();
Code:
0: getstatic #1 // Field $VALUES:[LEnumEntity;
3: invokevirtual #2 // Method "[LEnumEntity;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[LEnumEntity;"
9: areturn
public static EnumEntity valueOf(java.lang.String);
Code:
0: ldc #4 // class EnumEntity
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class EnumEntity
9: areturn
static {};
Code:
0: new #4 // class EnumEntity
3: dup
4: ldc #7 // String FIRST
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field FIRST:LEnumEntity;
13: new #4 // class EnumEntity
16: dup
17: ldc #10 // String SECOND
19: iconst_1
20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #11 // Field SECOND:LEnumEntity;
26: new #4 // class EnumEntity
29: dup
30: ldc #12 // String THIRD
32: iconst_2
33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
36: putstatic #13 // Field THIRD:LEnumEntity;
39: iconst_3
40: anewarray #4 // class EnumEntity
43: dup
44: iconst_0
45: getstatic #9 // Field FIRST:LEnumEntity;
48: aastore
49: dup
50: iconst_1
51: getstatic #11 // Field SECOND:LEnumEntity;
54: aastore
55: dup
56: iconst_2
57: getstatic #13 // Field THIRD:LEnumEntity;
60: aastore
61: putstatic #1 // Field $VALUES:[LEnumEntity;
64: return
}
枚举类常用方法
int compareTo(E o): 该方法用于与制定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;反之返回负整数;否则返回零。
static values(): 返回一个包含全部枚举值的数组,可以用来遍历所有枚举值。
String name(): 返回此枚举实例的名称,即枚举值。
ordinal():返回实例声明时的次序,从 0 开始。
static valueOf(): 返回带指定名称的指定枚举类型的枚举常量,名称必须与在此类型中声明枚举常量所用的标识符完全匹配
boolean equals()方法: 比较两个枚举类对象的引用。
注:因为枚举量在JVM中只会保存一个实例,所以枚举常量比较直接使用"=="比较即可
枚举类综合案例:
1、根据枚举值获取枚举对象
public enum EnumEntity {
/**
* (1)下面定义了无参枚举和有参枚举变量,***枚举变量
* 必须写在所有其他成员基本变量(int String...)的之前***
* 每个枚举变量都是EnumEntity的实例,每个成员变量
* 都是final static修饰,可以直接 类名.变量名 使用;
* (2)每个枚举变量都有一个默认的序号,下标从零开始;
* (3)若定义了有参枚举变量,那么也要添加相应的有参
* 构造函数。默认都是private修饰不可修改,不可实例化
*/
FIRST,SECOND,THIRD,
ONE("first"),TWO("second"),THREE("second"),
SPRING("Mar", "春天"), SUMMER("June", "夏天"), AUTUMN("Sep", "秋天");
private String typeName;
private String month;
private String season;
/**
* 实例化对象的时候,隐式自动调用构造方法,无参调无参,有参调有参
*/
EnumEntity(){
}
EnumEntity(String typeName) {
this.typeName = typeName;
}
EnumEntity(String month, String season) {
this.month = month;
this.season = season;
}
/**
* 根据类型名称返回枚举实例
* @param typeName 类型名称
* @return Enum
*/
public static EnumEntity fromTypeNameToType(String typeName) {
EnumEntity[] values = EnumEntity.values();
for (EnumEntity value : values) {
if (value.typeName != null) {
if (typeName.equals(value.typeName))
return value;
}
}
return null;
}
geter...setter
}
2、枚举单例
3.6 反射
什么是反射?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
3.6.1 Class对象
在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识,这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。然而可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class。
获取Class对象的方法
-
**
Class.forName("全类名");
**将字节码文件加载进内存,返回Class对象 -
- 多用于配置文件,将类名定义在配置文件中。读取文件,加载类
-
类名.class
通过类名的属性class获取 -
- 多用于参数的传递
-
对象.getClass()
-
- 多用于对象的获取字节码的方式
- 基本类型的封装类是通过
封装类.TYPE
来获取对应的基本类型的类实例
注意:
1)一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。比如,int不是类,但int.class是Class类型的对象。比如说获取int类型的类实例应该是int.class,而不是Integer.class,Integer是int的封装类。
2)同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的class对象都是同一个。
3)Class类实际上是一个泛型类。只不过它将已经抽象的概念更加复杂化了。
利用反射分析类的结构
public class ReflectionTest {
public static void main(String[] args) throws ClassNotFoundException {
String cssName = null;
if (args.length > 0) {
cssName = args[0];
} else {
Scanner scanner = new Scanner(System.in);
cssName = scanner.next();
}
Class<?> cls = Class.forName(cssName);
Class<?> supercl = cls.getSuperclass();
String modifies = Modifier.toString(cls.getModifiers());
if (modifies.length() > 0) {
System.out.print(modifies + " ");
}
System.out.print("class " + cls.getName());
if (supercl != null && supercl != Object.class) {
System.out.print(" extends " + supercl.getName());
}
System.out.print("\n{\n");
printConstructors(cls);
System.out.println();
printMethods(cls);
System.out.println();
printFields(cls);
System.out.println("}");
}
private static void printConstructors(Class cl) {
Constructor[] constructors = cl.getDeclaredConstructors();
for (Constructor c : constructors) {
String name = c.getName();
System.out.print(" ");
String modifiers = Modifier.toString(c.getModifiers());
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(name + "(");
Class[] parameterTypes = c.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
if (i > 0)
System.out.print(", ");
System.out.print(parameterTypes[i].getName());
}
System.out.println(");");
}
}
private static void printMethods(Class cl) {
Method[] methods = cl.getDeclaredMethods();
for (Method m : methods) {
Class<?> retType = m.getReturnType();
String name = m.getName();
String modifiers = Modifier.toString(m.getModifiers());
System.out.print(" ");
// print modifies, return type and method name
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(retType.getName() + " " + name + "(");
Class[] parameterTypes = m.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
if (i > 0)
System.out.print(", ");
System.out.print(parameterTypes[i].getName());
}
System.out.println(");");
}
}
private static void printFields(Class cl) {
Field[] fields = cl.getDeclaredFields();
for (Field f : fields) {
Class<?> type = f.getType();
String modifiers = Modifier.toString(f.getModifiers());
System.out.print(" ");
if (modifiers.length() > 0) {
System.out.print(modifiers + " ");
}
System.out.print(type.getName());
System.out.println(";");
}
}
}
---------------------------test java.lang.Double---------------------------
// 打印结果
public final class java.lang.Double extends java.lang.Number
{
public java.lang.Double(double);
public java.lang.Double(java.lang.String);
public boolean equals(java.lang.Object);
public static java.lang.String toString(double);
public java.lang.String toString();
public int hashCode();
public static int hashCode(double);
public static double min(double, double);
public static double max(double, double);
public static native long doubleToRawLongBits(double);
public static long doubleToLongBits(double);
public static native double longBitsToDouble(long);
public volatile int compareTo(java.lang.Object);
public int compareTo(java.lang.Double);
public byte byteValue();
public short shortValue();
public int intValue();
public long longValue();
public float floatValue();
public double doubleValue();
public static java.lang.Double valueOf(java.lang.String);
public static java.lang.Double valueOf(double);
public static java.lang.String toHexString(double);
public static int compare(double, double);
public static boolean isNaN(double);
public boolean isNaN();
public static boolean isFinite(double);
public static boolean isInfinite(double);
public boolean isInfinite();
public static double sum(double, double);
public static double parseDouble(java.lang.String);
public static final double;
public static final double;
public static final double;
public static final double;
public static final double;
public static final double;
public static final int;
public static final int;
public static final int;
public static final int;
public static final java.lang.Class;
private final double;
private static final long;
}
Skill Points:
反射机制相关的类
类名 | 用途 |
---|---|
Class类 | 代表类的实体,在运行的Java应用程序中表示类和接口 |
Field类 | 代表类的成员变量(成员变量也称为类的属性) |
Method类 | 代表类的方法 |
Constructor类 | 代表类的构造方法 |
Modifier(修饰符相关类)
* 返回对应modifiers中位设置的修饰符的字符串表示
static String toString(int modifiers);
* 检测方法名中对应的修饰符在modifiers值中的位
static boolean is修饰符(int modifiers);
4. 接口
4.1 接口概述
1)什么是接口?
接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为。
**现实中的接口:**接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。换句话说,接口就是制定各种功能特征的规范,而不实现这些规范的细节。
public interface InterfaceOne {
int a = 10;
void say();
}
// 反编译后的代码
// class version 52.0 (52)
// access flags 0x601
public abstract interface InterfaceOne {
// compiled from: InterfaceOne.java
// access flags 0x19
public final static I a = 10
// access flags 0x401
public abstract say()V
}
通过反编译之后,我们可以得知接口的特性
2)接口的特性
- 接口中方法默认是抽象方法,且会自动的添加
public abstract
- **在接口中声明的变量其实都是常量,**将隐式地声明为
public static fina
,所以接口中定义的变量必须初始化。不允许使用其他修饰符(public、default除外JDK8)。 - 接口中没有构造方法,不能被实例化
- 接口支持多继承(必须继承接口而非类)
4.2 抽象类
抽象类:abstract修饰的类叫做抽象类,类是具体事物的抽象,而抽象类则是对类的抽象,也可以说是对具体事物的进一步抽象。
抽象类的约定
1)abstract修饰,类中方法访问修饰符不允许为private,只能是public或protected。(private组织了创建抽象类让其他类继承从而实现方法)
2)不能被实例化,可以有成员变量、成员方法、构造方法。
3)继承抽象方法的子类必须重写该方法,否则该子类也必须声明为抽象类。
4)抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类。
5)继承体系是一种is…a的关系
总之,抽象类除了不能被实例化和其他的一些小细节外,其余的和普通的类用法没有什么区别
4.3 抽象类与接口的区别
**接口是对动作(行为)的抽象,抽象类是对类(事物本质)的抽象。**抽象类表示的是这个对象是什么,接口表示的是这个对象能做什么。
抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
不同点:
- 接口解决了java单继承的问题,抽象类不行
- 接口只有方法的定义,不能有方法的实现,
java 1.8中可以定义default方法体
,而抽象类可以有定义与实现,方法可在抽象类中实现。 - 接口强调特定功能的实现,而抽象类强调所属关系(is-a)。
设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。
而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。
如何选择抽象类和接口?
根据上述理解去选择,事物选择类,行为动作选接口。
4.4 jdk8接口新特性
1)接口中可定义静态方法
静态方法必须通过接口类进行调用
2)接口中可定义默认方法
default方法必须通过实现类的对象来调用
注:如果多个接口中存在同样的static和default方法,并且一个实现类中实现了多个这样的接口,会怎样?
static函数签名相同的方法:编译正常通过,因为静态方法可以直接通过类名调用。
default函数签名相同的方法:编译报错,实现类必须重写该方法,解决二义性。如果想调用父类方法,可以通过接口名.super.function
4.5 接口示例(Arrays.sort)
java.lang.Comparable<T>
int compareTo(T other)
用这个对象与other进行比较。如果这个对象小于 other则返回负值,如果相等则返回0;否则返回正值
java.util.Arrays
static void sort(Object[] a)
使用mergesort算法对象数组a中的元素进行排序。 要求数组中的元素必须属于实现了Comparable接口的类,并 且元素之间必须是可比较的。【默认升序排序】
下面是对Employee数组进行排序的代码
public class Employee implements Comparable<Employee>{
static {
//System.out.println("Employee static ...");
}
private String name;
private double salary;
private LocalDate hireDay;
...
@Override
public int compareTo(Employee o) {
return Double.compare(this.salary, o.salary);
}
}
// 测试
public void testComparable() {
Employee[] employees = new Employee[3];
employees[0] = new Employee("Xiu", 3000, 2020, 11, 10);
employees[1] = new Employee("Gang", 5000, 2020, 11, 10);
employees[2] = new Employee("Long", 2000, 2020, 11, 9);
Arrays.sort(employees);
}
Comparator接口
前面,我们如果想要对自定义类的对象进行排序,只需要重写Comparable接口即可;String类默认实现了Comparable接口,并且排序规则是按照字典顺序排序,现在有一需求,将字符串数组根据字符串的长度进行排序,这时就需要用到另一个Arrays.sort相关的API
public static <T> void sort(T[] a, Comparator<? super T> c)
自定义LengthComparator
实现Comparator接口
class LengthComparator implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
// 测试
public void testComparable() {
LengthComparator lengthComparator = new LengthComparator();
String[] friends = {"Peter", "Paul", "Mary"};
System.out.println("sorted by dic -->" + Arrays.toString(friends));
Arrays.sort(friends, lengthComparator);
System.out.println("sorted by length -->" + Arrays.toString(friends));
}
4.6 lambda表达式
下面部分章节选自 —《On Java 8》
顺序阅读可能会有点困难,因为一开始就用了一些lambda相关的语法,推荐先整体阅读,之后返回到懵懂点,再次阅读。
推荐阅读《On Java 8》、《Effective Java》
4.6.1 函数式编程和闭包
函数式编程(英语:functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算(lambda calculus)为该语言最重要的基础。而且,λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。
比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
在函数式编程中,函数是第一类对象,意思是说一个函数,既可以作为其它函数的参数(输入值),也可以从函数中返回(输入值),被修改或者被分配给一个变量。
函数式语言的特性
- 高阶函数(Higher-order function)
- 偏应用函数(Partially Applied Functions)
- 柯里化(Currying)
- 闭包(Closure)
Summary:(函数式和面向对象的区别【未完待续】)
函数式编程将函数作为解决问题的出发点,以函数的思维去解决问题。这种编程的基础是λ演算,接收函数作为输入和输出。【强调:做什么】
面向对象编程将事物看做是由属性和行为组成的,以对象为核心去处理问题。【强调:怎么做】
**闭包:**内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。
闭包三要素:函数、自由变量、环境
突然发现这里面水好深,停更,未完待续!!!
4.6.2 新旧对比
通常,传递给方法的数据不同,结果不同。如果我们希望方法在调用时行为不同,该怎么做呢?JDK8之前,我们可以通过在方法中创建包含所需行为的对象,然后将对象传递给我们想要控制的方法来完成操作即可。下面我们用传统形式和Java8的方法引用、Lambda表达式分别演示。
// functional/Strategize.java
interface Strategy {
String approach(String msg);
}
class Soft implements Strategy {
public String approach(String msg) {
return msg.toLowerCase() + "?";
}
}
class Unrelated {
static String twice(String msg) {
return msg + " " + msg;
}
}
public class Strategize {
Strategy strategy;
String msg;
Strategize(String msg) {
strategy = new Soft(); // [1]
this.msg = msg;
}
void communicate() {
System.out.println(strategy.approach(msg));
}
void changeStrategy(Strategy strategy) {
this.strategy = strategy;
}
public static void main(String[] args) {
Strategy[] strategies = {
new Strategy() { // [2]
public String approach(String msg) {
return msg.toUpperCase() + "!";
}
},
msg -> msg.substring(0, 5), // [3]
Unrelated::twice // [4]
};
Strategize s = new Strategize("Hello there");
s.communicate();
for(Strategy newStrategy : strategies) {
s.changeStrategy(newStrategy); // [5]
s.communicate(); // [6]
}
}
}
// 输出结果
hello there?
HELLO THERE!
Hello
Hello there Hello there
Strategy 接口提供了单一的 approach()
方法来承载函数式功能。通过创建不同的 Strategy 对象,我们可以创建不同的行为。
传统上,我们通过创建一个实现 Strategy 接口的类来实现此行为,比如在 Soft。
- [1] 在 Strategize 中,Soft 作为默认策略,在构造函数中赋值。
- [2] 一种略显简短且更自发的方法是创建一个匿名内部类。即使这样,仍有相当数量的冗余代码。你总是要仔细观察:“哦,原来这样,这里使用了匿名内部类。”
- [3] Java 8 的 Lambda 表达式。由箭头
->
分隔开参数和函数体,箭头左边是参数,箭头右侧是从 Lambda 返回的表达式,即函数体。这实现了与定义类、匿名内部类相同的效果,但代码少得多。 - [4] Java 8 的方法引用,由
::
区分。在::
的左边是类或对象的名称,在::
的右边是方法的名称,但没有参数列表。 - [5] 在使用默认的 Soft strategy 之后,我们逐步遍历数组中的所有 Strategy,并使用
changeStrategy()
方法将每个 Strategy 放入 变量s
中。 - [6] 现在,每次调用
communicate()
都会产生不同的行为,具体取决于此刻正在使用的策略代码对象。我们传递的是行为,而非仅数据。[^3]
在 Java 8 之前,我们能够通过 [1] 和 [2] 的方式传递功能。然而,这种语法的读写非常笨拙,并且我们别无选择。方法引用和 Lambda 表达式的出现让我们可以在需要时传递功能,而不是仅在必要才这么做。
4.6.3 lambda初识
个人理解
lambda表达式 :接口中特定方法的快速实现。两个点,一必须是函数式接口,二特定方法单一抽象方法(SAM)。lambda表达式属于函数式编程的思想,通过写lambda表达式,lambda更多的是强调做什么,结果是什么,只关心核心。面向对象解决问题的核心关注带你是怎么做。
lambda表达式的应用场景:
1)简化匿名内部类
在匿名内部类中,有许多内容都是冗余的,对业务处理是没有一毛联系的。整个匿名内部类中最关键的东西是方法,方法中最关键的有参数、方法体、返回值。lambda就是根据这三个核心内容,简化匿名内部类。
// 比如常见的匿名内部类格式:
new 父类或接口() {
//重写的方法
}
// 比如Runnable接口
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行了");
}
}).start();
// lambda格式
new Thread(() -> System.out.println(Thread.currentThread().getName() + "执行了")).start();
4.6.5函数式接口
对于只有一个抽象方法的接口(SAM Single Abstract Method),需要这种接口的对象时,可以提供一个lambda表达式。这种接口称为函数式接口(functional interface),@FunctionInterface标注(非必须)。
# 为什么定义只有一个抽象方法,接口中的方法不都是抽象的吗?
* java8接口新增default、静态方法,这些新特性有可能使接口中的方法不在是抽象的。另外重写Object中的默认方法(equals、String)不会计入接口中的抽象方法。
例如
public interface IExample
default void post() {...};
static void delay() {...};
boolean equals(Object obj);//父类中的方法,不会纳入接口中的抽象方法
}
上面接口的方法不在是抽象的,并且也没有抽象方法。
# 为什么interface可以包括Object类的方法的抽象声明重写?
* Object是所有类的默认父类,也就是说任何对象都会包含Object里面的方法,即使是函数式接口的实现,也会有Object的默认方法,所以,重写Object中的方法,不会计入接口方法中,除了final不能重写的,Object中所能重写的方法,写到接口中,不会影响函数式接口的特性
* 换个角度说,interface也默认继承了Object类;这里可以把inteface看作一个抽象类的极致,也就是说本质也是一个类,这样就可以继承Object类。但interface只是声明了Object中的方法,具体实现还是靠interface的实现类,写一个interface引用是可以调用Object类中的方法的,但因为没有实现,所以运行会报错;;;但因为它的实现类也继承了Object类,所以相当于重写了Object中的方法的interface只是一个中介,连接了Object与接口的实现类。
JDK提供的函数式接口
// JDK8之前符合函数式的接口
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
// JDK8新增重磅函数式接口 java.util.function.*包下的接口都是函数式接口
java.util.function.*
下面我们重点来认识一下java.util.function.*
包下的函数式接口以及如何使用它们。
Supplier-生产型接口
特定类型对象实例的提供者。无参数,返回类型任意。指定接口的泛型是什么类型,那么接口的get方法就会生产什么类型的数据,有返回值。
@FunctionalInterface
public interface Supplier<T> {
T get();
}
示例:
// 还是前面的Employee例子
//Supplier<Employee> supplier = Employee::new; 空参构造函数
Supplier<Employee> supplier = () -> new Employee("Long", 5000D, 2020,7, 7);
Employee employee = supplier.get();
IntSupplier intSupplier = () -> 3*5;
int asInt = intSupplier.getAsInt();
Consumer 消费型接口
消费一个指定泛型的数据,无返回值。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
/**
* 返回一个组合的{@code Consumer},它依次执行此accept操作,然后执行{@code after}操作。如果执行操作 * 中的任何一个引发异常,则将其中传递给合成操作的调用方。如果执行此操作引发异常,则将不会执行{@code * after}操作。
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
默认方法andThen
:如果一个方法的参数和返回值全部是Consumer类型,那么就可以实现:消费数据的时候,首先做一个操作,然后在做一个操作,实现组合,而这个方法就是Consumer接口中的default方法
Consumer<Employee> name = e -> e.setName("001");
Consumer<Employee> salary = e -> e.setSalary(5000.0);
Consumer<Employee> hireDay = e -> e.setHireDay(LocalDate.now());
name.andThen(salary).andThen(hireDay)
.andThen(System.out::println).accept(supplier.get());
//Employee{name='001', salary=5000.0, hireDay=2020-07-17}
Predicate 判断接口
判断是否的接口
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
/**
*返回表示该谓词和另一个谓词的短路逻辑* AND的组合谓词。在评估组成的谓词时,如果此谓词为{@code false}, *则不会评估{@code other} *谓词。
* 相当于&&功能
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
// 非运算
default Predicate<T> negate() {
return (t) -> !test(t);
}
// 或运算
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
// 判相等
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
示例:
Predicate<LocalDate> year = y -> LocalDate.now().getYear() == y.getYear();
Predicate<LocalDate> month = m -> m.getMonth() == LocalDate.now().getMonth();
Predicate<LocalDate> day = d -> d.getDayOfMonth() == LocalDate.now().getDayOfMonth();
boolean isToday = year.and(month).and(day).test(LocalDate.of(2020, 7, 17));
//boolean isYesterday = year.and(month).and(day).test(LocalDate.of(2020, 7, 16));
System.out.println("isToday-->" + isToday); // true
Predicate<Integer> exp1 = i -> i > 0;
Predicate<Integer> exp2 = i -> {
System.out.println("exp2");
return true;
};
// 测试 or and短路功能
System.out.println(exp1.and(exp2).test(-1));//false
System.out.println(exp1.or(exp2).test(-1));//exp2 true
System.out.println(exp1.and(exp2).test(1));//exp2 true
System.out.println(exp1.negate().and(exp2).test(-1));//exp2 true
// equals是Predicate中的静态方法
Predicate<String> s = Predicate.equals("hello");
s.test("hello");//true
Function 数据类型转换接口
根据一个数据类型得到另一个数据类型。在使用默认方法时,注意默认方法的形参类型,一定要类型匹配。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// 和andThen执行顺序相反,它先执行before,后执行所调用的
// 将后来的输出作为先前的输入,后来的输出的类型必须是Function输入所定义的类型
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
// 学习完泛型再回过头来看就感觉好点
// 将先前的输出作为后来的输入
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
示例:
Function<String, Integer> func = Integer::parseInt;
Integer apply = func.apply("1024");
//Function<String,Integer> -> String->T,Integer->R,将String类型作为输入,Integer类型作为输出
//andThen函数形参也是一个函数式接口,函数原型在上面展示了,
//andThen(Function<? super T, ? extends V)); i -> i*2就是将func的输出(Integer)作为输入(Integer),输出又定义了一个新的参数类型V
func.andThen(i -> i*2).apply("1024");
Function<Integer, Integer> func1 = i -> ++i;
//func.compose(func1); error 输入参数类型T不一致
Integer res = func1.compose(func).apply("1024");
//看下面这几个例子
Function<String, Long> func = Long::parseLong;
String res = func.andThen(String::valueOf).apply("123");
Function<Long, String> func1 = l -> String.valueOf(l*2);
Long res1 = func.compose(func1).apply(123L);
4.6.4 走进lambda
基本语法
// functional/LambdaExpressions.java
interface Description {
String brief();
}
interface Body {
String detailed(String head);
}
interface Multi {
String twoArg(String head, Double d);
}
public class LambdaExpressions {
static Body bod = h -> h + " No Parens!"; // [1]
static Body bod2 = (h) -> h + " More details"; // [2]
static Description desc = () -> "Short info"; // [3]
static Multi mult = (h, n) -> h + n; // [4]
static Description moreLines = () -> { // [5]
System.out.println("moreLines()");
return "from moreLines()";
};
public static void main(String[] args) {
System.out.println(bod.detailed("Oh!"));
System.out.println(bod2.detailed("Hi!"));
System.out.println(desc.brief());
System.out.println(mult.twoArg("Pi! ", 3.14159));
System.out.println(moreLines.brief());
}
}
任何 Lambda 表达式的基本语法是:
- 参数。
- 接着
->
,可视为“产出”。 ->
之后的内容都是方法体。- [1] 当只用一个参数,可以不需要括号
()
。 然而,这是一个特例。 - [2] 正常情况使用括号
()
包裹参数。 为了保持一致性,也可以使用括号()
包裹单个参数,虽然这种情况并不常见。 - [3] 如果没有参数,则必须使用括号
()
表示空参数列表。 - [4] 对于多个参数,将参数列表放在括号
()
中。
- [1] 当只用一个参数,可以不需要括号
到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 return 关键字是非法的。 这是 Lambda 表达式缩写用于描述功能的语法的另一种方式。
[5] 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 return。
为什么可以那样写就是接下来说的类型推断。
类型推断
lambda类型推断的依据一方面根据函数接口的类型从而推断参数类型,是否有返回值,返回值类型等。所以说lambda表达式可以简写到什么地步,可以根据实现的函数接口来判断。但是类型推断机制并不是完美无瑕的,当编译不能识别类型的时候,这时候必须显示给出类型。
lambda表达式简写依据:
1)如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型
2)如果方法只有一个参数,而且这个参数的类型可以推导出,那么甚至可以省略小括号
3)无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推倒得出。
4)当只有一个参数时,圆括号可以省略 param -> {...}
,当无参数时,必须有括号() -> {...}
,括号和单参可以看成是一个构成lambda的标识,如果空参的lambda这样写-> {...}
,编译器肯定不认啊。
这时候在回过头去看基本语法中刚开始的例子就会有所感悟,下面给出了自己当时写的例子。
public class LambdaTest {
public static void main(String[] args) {
// 完整声明
Arithmetic addOperation = (int a, int b) -> {
// you can do everything in here.
return a + b;
};
// 省略
Arithmetic minusOperation = (a, b) -> a - b;
// 无参数有返回值
ArithmeticNoParam noParam = () -> 1 + 2;
// 无参数无返回值
ArithmeticNoParamVoid noParamVoid = () -> System.out.println(Integer.MAX_VALUE);
// 单参数有返回值
LogicOperation logicOperation = b -> !b;
}
interface Arithmetic {
int arithmetic(int a, int b);
}
interface ArithmeticNoParam {
int arithmetic();
}
interface ArithmeticNoParamVoid {
void arithmetic();
}
interface LogicOperation {
boolean logicOperation(boolean b);
}
}
方法引用*
what? why? how? ❓❓❓
1)什么是方法引用
方法引用是lambda表达式的一种特殊形式,当我们要编写一个lambda表达式时,发现已经有一个方法实现了我们在lambda表达式中要实现的功能,那么可以直接使用方法引用进行替换。
2)为什么使用
lambda 优于匿名类的主要优点是它们更简洁。Java 提供了一种生成函数对象的方法,它比 lambda 更简洁:方法引用。
方法引用通常提供一种更简洁的 lambda 替代方案。在使用方法引用可以更简短更清晰的地方,就使用方法引用,如果无法使代码更简短更清晰的地方就坚持使用 lambda。(Where method references are shorter and clearer, use them; where they aren’t, stick with lambdas.)
3)上手
方法引用的分类
- 类名::静态方法名
- 对象::实例方法名 (绑定的方法引用)
- 类名::实例方法名 (未绑定的方法引用)
- 类名::new (构造器引用)
在绑定引用中,接收对象在方法引用中指定。绑定引用在本质上类似于静态引用:函数对象采用与引用方法相同的参数。在未绑定的引用中,在应用函数对象时,通过方法声明的参数之前的附加参数指定接收对象。未绑定引用通常用作流管道(stream pipelines)(第 45 项)中的映射和过滤功能。最后,对于类和数组,有两种构造函数引用。构造函数引用充当工厂对象。
—摘自《Effective Java》
3.1 类名::静态方法名
首先,定义一个Student类
public class Student {
String name;
int score;
public Student() {}
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public static int compareByScore(Student s1, Student s2) {
return s1.score - s2.score;
}
public int compareByName(Student s2) {
return this.name.compareTo(s2.name);
}
// toString...
}
类中提供了一个返回两个学生成绩大小的静态方法和一个按字典顺序比较两个学生姓名的成员方法。接下来我们对学生进行按成绩排序,首先我们先使用lambda表达式处理:
public static void main(String[] args) {
Student s1 = new Student("Long", 90);
Student s2 = new Student("XiuYu", 80);
Student s3 = new Student("MingSh", 70);
Student[] students = {s1, s2, s3};
Arrays.sort(students, (x, y) -> x.score - y.score);
System.out.println(Arrays.toString(students));
//[Student{name='MingSh', score=70}, Student{name='XiuYu', score=80}, Student{name='Long', score=90}]
}
Arrays.sort
第二个参数接受一个Comparator函数式接口,接口中的唯一抽象方法compare接收两个参数返回一个int类型值,比较的具体逻辑由lambda表达式给出即(x, y) -> x.score - y.score
,前面我们已经提到过,当lambda表达式所实现的功能之前已经有实现的方法了,那么可以直接用方法引用代替。有没有返回两个学生成绩的大小呢?已经有的了,在Student类中正好定义着一个静态方法,方法的形参和返回值正好和Comparator中的抽象方法相对应,所以我们可以使用 Student::compareByScore
来代替lambda表达式
//Arrays.sort(students, (x, y) -> x.score - y.score);
Arrays.sort(students, Student::compareByScore);
3.2 对象::实例方法名
绑定引用在本质上类似于静态引用:函数对象采用与引用方法相同的参数。
在Student类中新增成员方法
public int compareByScoreNoStatic(Student s1, Student s2) {
return s1.score - s2.score;
}
然后通过对象实例方法名引用
Student s = new Student();
Arrays.sort(students,s::compareByScoreNoStatic);
现在再回过头去理解引用的那句话,实例对象并没有作为方法的参数,它和静态方法本质上是一样的,方法参数对应函数式接口中的方法参数。
接下来我们在看类名::方法名,它会有什么不同点。
3.3 类名::实例方法名
这种方法引用和前两种有所不同,因为无论是通过类名调用静态方法和对象调用实例方法都是符合常规,并且方法参数和返回类型都符合函数式接口中的SAM。Student类中的compareByScore
看似没什么问题,但是由于其是静态方法,所以在任何地方都可以调用,而不单单是从属于Student实例了,如果定义非静态的,那么类的封装性更好一些,那么又会存在方法参数的问题,如果签名为compareByScoreNoStatic(Student s1, Student s2)
那不就和对象::方法名那种引用方式重复了吗,因为实例方法隐式持有this引用,我可以通过this来确定一个对象和另一个对象。所以说就有了如下:
public static int compareByScore(Student s1, Student s2) {
return s1.score - s2.score;
}
// -->非静态
public int compareByScoreNoStatic(Student s) {
return this.score - s2.score;
}
之后,我们可以通过 类名::实例方法名 这种形式的方法引用代替lambda,当使用 类名::实例方法名 方法引用时,lambda表达式所接收的第一个参数来调用实例方法,如果lambda表达式接收多个参数,其余的参数作为方法的参数传递进去。所以说在使用未绑定的方法引用时,必须提供调用方法的对象。
//Arrays.sort(students, (x, y) -> x.score - y.score);
Arrays.sort(students, Student::compareByName);
看到这可能就有疑问了,实现的方法有两个参数,为什么这里就有一个。其实它等价于Arrays.sort(students, (x, y) -> x.compareByName(y));
参数实质并没有变。
思考:Comparator comparator = Comparator.comparingInt(String::length);
函数式接口,方法引用,函数签名不在和ToIntFunction中的int applyAsInt(T t);签名相同。
3.4 构造方法引用
这个就简单多了,直接看实例
Student下新增一个接口,通过类名::new
的形式提供一个构造函数的引用
public interface Creature {
void instantiate();
}
public static void main(String[] args) {
Creature creature = Student::new;
creature.instantiate();
}
总结
如何判断自己掌握了lambda的语法和方法引用,如果你能够根据lambda表达式和方法引用的形式,反推回去,根据lambda和方法引用所需的形式能够自己构造出相应的函数式接口并且lambda或者方法化,那么说明你对lambda已经有所了解了。
附:函数式接口里面用到了大量的泛型,所以说当对泛型基础有一定的把握之后,一定要看一看函数式接口中的泛型方法,能进一步加深自己对泛型的理解。
public SummaryTest {
interface Concat {
String concat(String thisObj, String str);
}
interface ConcatAno {
String concatAno(String str);
}
interface ToUpper {
String toUpper();
}
public static void main(String[] args) {
// lambda表达式
String[] planets = {"Mercury", "Venus", "Mars", "Earth", "Jupiter"};
Arrays.sort(planets, (String x, String y) -> {
return x.length() - y.length();
});
Arrays.sort(planets, (String x, String y) -> x.length() - y.length());
Integer[] degree = {1, 3, 5, 7, 9};
// lambda表达式 类型自动推断,无需return
Arrays.sort(planets, (x, y) -> x.length() - y.length());
// 方法引用
// 类名::静态方法 public static int compare(int x, int y)
Arrays.sort(degree, Integer::compare);
// 类名::实例方法 注意区别
Arrays.sort(degree, Integer::compareTo);
Arrays.sort(planets, String::compareTo);
// 再次理解类名实例方法名
// public String concat(String str)方法签名不在与
// String concat(String obj, String str);相同,如果把Concat中的函数签名改为
// String concat(String str);在String::concat引用方法的时候会报错,尽管这时候
// 签名相同了,但是由于concat方法的执行必须有一个对象来调用,str只是形参不是执行
// 调用方法的对象,所以我们必须提供一个调用方法的对象,下面"hello"就是执行调用方法的对象,
// 如何hello就是调用方法的对象验证?如果将"hello"替换为NULL run下就明白了。
// Concat ano = String::concatAno; error 未提供调用方法的对象
Concat c = String::concat;
//Concat lambdaC = (obj, str) -> obj.concat(str); 结合lambda理解
String result = c.concat("hello", " world!");
System.out.println(result);//hello world!
// 对象实例方法名
String s = "to upper";
ToUpper toUpper = s::toUpperCase;
System.out.println(toUpper.toUpper());//TO UPPER
}
}
4.6.5 变量作用域
Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final或事实上是final(隐性final)。
示例:
// 对象中的实例变量 堆中保存,不会随方法销毁而销毁,延时(满足条件才会执行的)lambda使用实例变量没有问题.
// 但是如果使用的是局部变量,方法销毁,变量也会销毁,这时候变量必须是final类型的。
public class TimerTest {
int y;
interface Listener {
int click(int x);
}
public static void main(String[] args) {
TimerTest timerTest = new TimerTest();
timerTest.test();
}
private void test() {
int z = 10;
String s = "hello";
ArrayList<String> list = new ArrayList<>();
list.add("hello");
Listener listener = (x) -> {
x++;
// z++; error
list.add(" world");
return x+y++;
};
int res1 = listener.click(1);
System.out.println(res1);// 2
int res2 = listener.click(1);
System.out.println(res2);// 3
int res3 = listener.click(1);
System.out.println(res3);// 4
System.out.println(Arrays.toString(list.toArray()));
//[hello, world, world, world]
}
}
lambda引用变量使用注意
-
只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
-
局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)等同final效果。
等同 final 效果(Effectively Final)。这个术语是在 Java 8 才开始出现的,表示虽然没有明确地声明变量是
final
的,但是因变量值没被改变过而实际有了final
同等的效果。 如果局部变量的初始值永远不会改变,那么它实际上就是final
的。 -
不允许声明一个与局部变量同名的参数或者局部变量。
4.7 内部类
一个定义在另一个类中的类,叫做内部类
public class JavaClass {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
// 第一种访问内部类的方法,通过在Outer内部实例化Inner
outerClass.show();
// 第二种访问内部类方法,直接实例化内部类对象
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
innerClass.show();
}
}
class OuterClass {
private String tag = "OuterTag";
class InnerClass {
public void show() {
System.out.println(tag);
}
}
public void show() {
InnerClass innerClass = new InnerClass();
innerClass.show();
}
}
4.7.1 为什么使用内部类
也许,搞清楚为什么使用这种技术比会使用某项技术更能让人愿意去了解。内部类给开发带来了什么便利?什么情况下使用内部类?我的理解内部类解决了:
单继承问题,如果没有内部类提供的、可以继承多个具体的或抽象类的能力,一些设计与编程问题就很难解决。换个角度说,内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(类或抽象类)。
简化多余代码,比如匿名内部类,有时对于某个接口的实现,并不常用接口中的方法,或者只调用一次,这时我们就可以使用匿名内部类来代替声明类实现接口的操作,这肯定会带来冗余的代码,而前面所讲的lambda表达式也是一种代替匿名内部类很好地方案,但是并不是所有的接口抽象方法都可以使用lambda表达式(SAM才可以使用),所以这时候最好就是使用匿名内部类。
内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
内部类可以对同一个包中的其他类隐藏起来。
4.7.2 内部类的创建
语法:记住一个原则,非静态内部类默认会有一个外部类的引用,即依赖外部对象的创建,静态内部类独立于外部类而存在,不需要与外部类对象建立关联。
//在外部类外部(静态方法内|非静态方法内),创建非静态内部类对象
Outer.Inner in=new Outer().new Inner();
//在外部类外部(静态方法内|非静态方法内),创建静态内部对象
Outer.Inner in=new Outer.Inner();
//----------------------------------------------------------------------
//在外部类内部(非静态方法内)创建成员内部类对象,就像普通对象一样直接创建
Inner in=new Inner();
//在外部类内部(静态方法内)创建成员内部类对象
Outer.Inner in=new Outer().new Inner();
//在外部内部类(静态方法内|非静态方法内)创建静态内部类对象
Inner in=new Inner();
4.7.2 内部类的分类
成员内部类
成员内部类既可以访问自身的数据域,也可以访问它的外围类对象的数据域,对于外围类来说,要想访问内部类属性,必须先创建内部类对象。你可能会有疑问了这是为什么?一个重要的原因是内部类的对象会保留一个指向创建它的外部类的引用。
1)成员内部类内部不允许存在任何static变量或方法,但可以有常量。正如成员方法中不能有任何静态属性 (成员方法与对象相关、静态属性与类有关)!!!
为什么不允许存在任何static变量或方法?
静态变量是属于类的,成员变量和方法是属于对象的,非静态内部类可以看成是一个外部类的成员属性,依赖于对象的,因为静态属性随类而加载而不是随对象加载,所以如果内部类中存在静态属性,由于静态属性会随类加载,而内部类又是外部类的一个成员属性随外部类对象而加载,这样就会在没有外部类对象的情况下,却加载了内部类的静态成员属性,产生了矛盾。
在类加载的时候,静态属性和代码块会先加载,那么按照这个逻辑,非静态内部类里面的静态属性也要优先于这个内部类加载,但这个时候这个内部类都还没有初始化,这就出现矛盾了。
2)成员内部类是依附外部类的,只有创建了外部类才能创建内部类。
静态内部类(嵌套类)
关键字static可以修饰成员变量、方法、代码块、其实还可以修饰内部类,使用static修饰的内部类我们称之为静态内部类,静态内部类和非静态内部类之间存在一个最大的区别,非静态内部类在编译完成之后会隐含的保存着一个引用,该引用是指向创建它的外围类,但是静态类没有。没有这个引用就意味着:
1)静态内部类的创建不需要依赖外部类可以直接创建。
2)静态内部类不可以使用任何外部类的非static类(包括属性和方法),但可以存在自己的成员变量。
局部内部类
局部内部类使用的比较少,其声明在一个方法体 / 一段代码块的内部,而且不在定义类的定义域之内便无法使用,其提供的功能使用匿名内部类都可以实现,而本身匿名内部类可以写得比它更简洁,因此局部内部类用的比较少。来看一个局部内部类的小例子:
public class InnerClassTest {
public int field1 = 1;
protected int field2 = 2;
int field3 = 3;
private int field4 = 4;
public InnerClassTest() {
System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");
}
private void localInnerClassTest() {
// 局部内部类 A,只能在当前方法中使用
class A {
// static int field = 1; // 编译错误!局部内部类中不能定义 static 字段
public A() {
System.out.println("创建 " + A.class.getSimpleName() + " 对象");
System.out.println("其外部类的 field1 字段的值为: " + field1);
System.out.println("其外部类的 field2 字段的值为: " + field2);
System.out.println("其外部类的 field3 字段的值为: " + field3);
System.out.println("其外部类的 field4 字段的值为: " + field4);
}
}
A a = new A();
if (true) {
// 局部内部类 B,只能在当前代码块中使用
class B {
public B() {
System.out.println("创建 " + B.class.getSimpleName() + " 对象");
System.out.println("其外部类的 field1 字段的值为: " + field1);
System.out.println("其外部类的 field2 字段的值为: " + field2);
System.out.println("其外部类的 field3 字段的值为: " + field3);
System.out.println("其外部类的 field4 字段的值为: " + field4);
}
}
B b = new B();
}
// B b1 = new B(); // 编译错误!不在类 B 的定义域内,找不到类 B,
}
public static void main(String[] args) {
InnerClassTest outObj = new InnerClassTest();
outObj.localInnerClassTest();
}
}
匿名内部类
**匿名内部类是一个没有名字的方法内部类,**它的特点有
1)匿名内部类必须继承一个类或者实现一个接口(类可以是抽象类,也可以是具体类)
2)匿名内部类没有类名,因此没有构造方法
个人觉得匿名内部类其实就是对接口简单快速的实现
推荐比较好的两篇文章:详解 Java 内部类、内部类定义
// 附加:具体类也可以用内部类实现
// 下面定义了一个Object的匿名内部类,新增了一个方法并调用
// 编译通过,并能运行
Object obj = new Object() {
public void func() {...}
}.func();
// 下面就不能通过编译
Object obj = new Object() {
public void func() {...}
};
obj.func();//compile error 没有该方法
第二个编译失败,因为匿名内部类是一个 子类对象,Object类型的obj指向了一个匿名子类内部,向上转型了,所以编译时会检查Object里面是否有func方法。
5. 异常、断言和日志
5.1 异常分类
如图所示,Throwable是所有异常的父类,往下划分为两大门派Error和Exception。
Error类是指Java运行时系统的内部错误和资源耗尽错误,应用程序不会抛出这种类型的对象。如果出现这种错误,听天由命…
Exception又分解为两个分支,可以看做是受查异常和非受查异常或者是其他分支和RuntimeException;程序错误导致:错误的类型转化、数组访问越界、空指针等都属于运行时异常,“如果出现RuntimeException异常,那么就一定是你的问题”很有道理。受查异常必须被处理,否则编译器报错。
Java语言将派生于Error类或RuntimeException类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked)异常。
public class ExceptionTest {
/**
* 第一个疑问点?
* 当在方法内部throw之后,有时方法会有throws,有时没有?
* 其实就是受查和非受查异常区别:
* 一般我们处理的都是受查异常
*/
public void writeData() throws IOException {
// IOException属于受查异常,必须处理,要么抛出要么捕获
//write...
throw new IOException();
/*
try {
throw new FileNotFoundException();
} catch (IOException e) {
e.printStackTrace();
}
*/
}
public void uncheck() {
// ClassCastException属于RuntimeException非受查异常
// 出现这种错误,就是代码有问题(数组下标越界..),
// 这种错误一般是在运行期发现的,由JVM抛出,我们人为不需要处理,
// 如果我们知道数组下标越界,为什么还不修改代码。
throw new ClassCastException();
// throw new RuntimeException();
}
}
常用非受查异常
异常 | 描述 |
---|---|
ArithmeticException | 当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。 |
ArrayIndexOutOfBoundsException | 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。 |
ArrayStoreException | 试图将错误类型的对象存储到一个对象数组时抛出的异常。 |
ClassCastException | 当试图将对象强制转换为不是实例的子类时,抛出该异常。 |
IllegalArgumentException | 抛出的异常表明向方法传递了一个不合法或不正确的参数。 |
IllegalMonitorStateException | 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程。 |
IllegalStateException | 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下。 |
IllegalThreadStateException | 线程没有处于请求操作所要求的适当状态时抛出的异常。 |
IndexOutOfBoundsException | 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。 |
NegativeArraySizeException | 如果应用程序试图创建大小为负的数组,则抛出该异常。 |
NullPointerException | 当应用程序试图在需要对象的地方使用 null 时,抛出该异常 |
NumberFormatException | 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。 |
SecurityException | 由安全管理器抛出的异常,指示存在安全侵犯。 |
StringIndexOutOfBoundsException | 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小。 |
UnsupportedOperationException | 当不支持请求的操作时,抛出该异常。 |
常用受查异常
异常 | 描述 |
---|---|
ClassNotFoundException | 应用程序试图加载类时,找不到相应的类,抛出该异常。 |
CloneNotSupportedException | 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常。 |
IllegalAccessException | 拒绝访问一个类的时候,抛出该异常。 |
InstantiationException | 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常。 |
InterruptedException | 一个线程被另一个线程中断,抛出该异常。 |
NoSuchFieldException | 请求的变量不存在 |
NoSuchMethodException | 请求的方法不存在 |
Throwable方法
序号 | 方法及说明 |
---|---|
1 | public String getMessage() 返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了。 |
2 | public Throwable getCause() 返回一个Throwable 对象代表异常原因。 |
3 | public String toString() 使用getMessage()的结果返回类的串级名字。 |
4 | public void printStackTrace() 打印toString()结果和栈层次到System.err,即错误输出流。 |
5 | public StackTraceElement [] getStackTrace() 返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。 |
6 | public Throwable fillInStackTrace() 用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中。 |
5.2 异常的处理
异常的处理要么捕获、要么抛出(异常声明)。
5.2.1 受查异常声明
受查异常的声明也是一种处理方案,利用throws声明方法可能抛出的异常,在调用此方法时必须对异常进行处理,要么捕获,要么继续向上级传递抛出,如果主方法上使用了throws抛出,则交给JVM处理,。
在编写方法时,不必将所有可能出现的异常都进行声明(throws)。什么异常使用throws子句进行声明,什么时候不需要,遵循下面原则:
- 调用一个抛出受查异常的方法
- 程序运行过程中发现错误,并且利用throw语句抛出的一个受查异常
- 不需要声明Java的内部错误,即从Error继承的错误。我们对其没有任何控制能力
一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制(Error),要么应该避免发生(RuntimeException)。
throw和throws的区别?
throw和throws都是在异常处理中使用的关键字,区别如下:
- throw:指的是在方法中人为抛出一个异常对象(这个异常对象可能是自己实例化或者抛出已存在的);
- throws:在方法的声明上使用,表示此方法在调用时必须处理异常。
5.2.2 异常捕获
Java提供了try、catch、finally
三个关键字来捕获处理异常。
(1)try后必须要有一个catch或者finally块,否则编译不通过。
(2)catch依附于try而存在,有catch必有try,同一个catch语句可以捕获多个异常。
(3)同级异常,catch不分前后,上下级(父子关系),上级放后(其实就是让异常的信息更具体)。
(4)try/catch是最常用的捕获异常的一种方案,finally非必须。
(5)无论是否发生异常或者try语句块存在return,finally里面的代码最终都执行。若finally里面也存在return,那么会覆盖catch或者try里面return的值。编写资源回收的代码通常在finally语句块中
public void readData() {
FileInputStream in = null;
//-----------示例一-----------
try {
in = new FileInputStream("/usr/data");
int read = in.read();
// FileNotFoundException 是IOException的子类,必须放在父类之前
// catch可以捕获多个异常,前提是不存在子类关系。异常之间的类无关联
} catch (FileNotFoundException | UnknownElementException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// finally最终都会执行
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//-----------示例二-----------
// 父类可以捕获其任何子类的异常
// 通过捕获更大范围的异常,可以减少catch语句的数量
try {
in = new FileInputStream("/usr/data");
int read = in.read();
// IOException可以捕获其子类的异常
} catch (IOException e) {
e.printStackTrace();
} finally {
// finally最终都会执行
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//-----------示例三-----------
// JDK1.7新增特性 (try-with-resources)带资源的try catch
// 假设资源属于AutoCloseable类型,try(Resources res=...){},当try语句块执行完毕之后
// 会自动调用res.close方法
try(FileInputStream fin = new FileInputStream("/user/data")) {
int read = fin.read();
} catch (IOException e) {
e.printStackTrace();
}
}
小结
// 向下面那样,finally里面又有try..catch很繁琐,对于处理IO流的关闭,如果项目中有很多这种代码块时,我们不得不考虑优化了
(1)try-with-resources
(2)自定义类继承自AutoCloseable接口,重写close方法,将异常提升到这里面。
finally {
// finally最终都会执行
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
5.3 自定义异常类 断言
在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。
- 所有异常都必须是 Throwable 的子类。
- 如果希望写一个检查性异常类,则需要继承 Exception 类。
- 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
按照国际惯例,自定义的异常应该总是包含如下的构造函数:
- 一个无参构造函数
- 一个带有String参数的构造函数,并传递给父类的构造函数。
- 一个带有String参数和Throwable参数,并都传递给父类构造函数
- 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。
编写自定义异常类,可以参考java源码,例如IOException
package java.io;
public class IOException extends Exception {
static final long serialVersionUID = 7818375828146090155L;
public IOException() {
super();
}
public IOException(String message) {
super(message);
}
public IOException(String message, Throwable cause) {
super(message, cause);
}
public IOException(Throwable cause) {
super(cause);
}
}
如何优雅的处理异常?