文章目录
- 1. 计算机语言概述
- 2. 跨平台原理
- 3. JDK&JRE
- 4. JDK 的下载与安装
- 5. 环境变量配置-临时配置方式
- 6. CLASSPATH 环境变量
- 7. 注释
- 8. 代码结构
- 9. 常量
- 10. 数据类型
- 11. var 关键字
- 12. 运算
- 13. 面向对象
- 14. Java核心类
- 15. 异常处理
- 16. 反射
- 17. 注解
- 18. 泛型
- 19. 集合
- 规范
- FAQ
1. 计算机语言概述
1.1 JAVA 诞生
1995
年诞生于 SUN(S
tandford U
niversity N
etwork 斯坦福大学网络公司)
1.2 就业方向
JAVA EE
Android
JAVA ME已过时
1.3 Oracle 简介
开发语言:JAVA
服务端操作系统:Solaris
数据库:Oracle
1.4 微软简介
开发语言:C#
服务端操作系统:Windows 2008、Windows 10…(Windows XP是客户端操作系统)
数据库:SQL Server
1.5 关于苹果
手机端操作系统:IOS
PC 端操作系统:MAC
2. 跨平台原理
JVM(Java Virtual Machine):JAVA虚拟机
JVM 作为 JAVA 语言和操作系统之间的桥梁,使得 JAVA 语言具有了跨平台性
但 JVM 本身并不具备跨平台性,对于不同的操作系统有不同版本的 JVM
3. JDK&JRE
JRE(Java Runtime Environment Java 运行环境):仅需要运行 Java 程序时使用,包括JVM + 核心类库
JDK(Java Development Kit Java 开发工具包):面向开发人员,包括JRE + 开发工具
,典型的开发工具包括编译工具(javac.exe)、打包工具(jar.exe)
4. JDK 的下载与安装
JAVA SE 最新版本
官网下载地址
JAVA SE 历史版本
官网下载地址
Windows操作系统下的 JDK 和 JRE 安装一次后,可以直接将生成的目录拷贝到 U 盘中,当做绿色版使用,不需要再次安装,但需要重新配置环境变量
JDK 本身内置了 JRE,而在 JDK 安装包中还含有独立的 JRE 安装程序,JDK 安装完毕后可以直接取消 JRE 安装
JDK 1.6 版本安装程序中,仅内置了 JRE 安装程序,不需要安装
JDK 1.7 版本安装程序中,内置了 JRE、 JAVA FX SDK(JAVA Software Developer Kit JAVA 软件开发者工具包)安装程序,都不需要安装
5. 环境变量配置-临时配置方式
通过 DOS 命令中的set
命令完成
- set
用于查看本机中的所有环境变量的值 - set path
仅查看 path 环境变量的值 - set path=值
给 path 环境变量赋值
注:若执行 set path= 会删除掉 path 环境变量
- set path=新值;%path%
在原来的 path 的值的基础上添加新值
JAVA_HOME 环境变量是必须创建的,为了避免因 JAVA 路径更换需要修改 path 环境变量存在的,有了 JAVA_HOME ,当 JAVA 路径更换需要更改环境变量时就只需要改变 JAVA_HOME 的值,而不需要对 path 做改动,因为 path 中很多是系统路径,修改过程中容易误操作导致系统路径出现问题
6. CLASSPATH 环境变量
CLASSPATH(classpath) 环境变量面向 JVM(java.exe 程序),指明了使用 java.exe 执行 class 文件时程序寻找 class 文件的路径,若不设置该环境变量,默认只在当前目录下查找
,若找到,则执行,若没有找到,则报错
接下来说明几种为 CLASSPATH 环境变量配置不同值的情况:
- CLASSPATH=E:\
直接去指定的路径下(E:\)查找 class 文件 - CLASSPATH=E:\;
除了去环境变量指定的目录下查找以外,找不到的时候会查找当前目录下 - CLASSPATH=.;E:\
先查找当前目录下,找不到的时候会接着到指定的目录下查找
2中当前目录为隐式指定,不推荐使用,3中为显示指定,推荐使用
javac 编译文件时需要携带 .java 后缀名,java 执行程序时不能带 .class 后缀名
对于一个类文件,若类的权限为 public ,则要求该类的 .java 文件名必须与类名保持一致
7. 注释
文档注释
/**
*/
单行注释
//
多行注释
/*
*/
文档注释,可以通过 bin 目录下 javadoc.exe 工具生成 API 文档
单行注释内可以嵌套单行注释、多行注释
多行注释
内只可以嵌套单行注释,不可以嵌套多行注释
注释经过编译后不会留在 .class 文件中
8. 代码结构
/*
需求说明:
......
思路:
......
步骤:
......
*/
具体代码及必要的行注释
9. 常量
分类:
- 整数常量
表现形式
1.1 二进制,以B
结尾表示,1001 0111B
1.2 八进制,以0
开头表示,0
123
1.3 十进制,123
1.4 十六进制 ,以0x
开头表示,0x
12 - 小数常量
- 布尔(boolean)常量:true 和 false
- 字符常量
- 字符串常量
- null 常量:null
10. 数据类型
Java中有两种数据类型,基本数据类型和引用数据类型(内存空间本身代表的是引用地址)
引用数据类型
10.1 基本数据类型
基本数据类型是CPU可以直接进行运算的类型。即内存空间本身代表的就是数据本身
- 整数类型:byte,short,int,long
- 浮点数类型:float,double
- 字符类型:char
- 布尔类型:boolean
不同的数据类型占用的字节数不同
JVM内部会把boolean表示为4字节整数
Java的char类型保存的是Unicode字符
11. var 关键字
省略变量类型,java10
以后支持
12. 运算
12.1 整数运算
加减乘除、求余、位运算、移位运算
整数的除法对于除数为0时运行时将报错,但编译不会报错。
整数由于存在范围限制,如果计算结果超出了范围,就会产生溢出,而溢出不会出错
移位运算
对byte和short类型进行移位时,会首先转换为int再进行移位。
类型自动提升
如果参与运算的两个数类型不一致,那么计算结果为较大类型的整型
强制转型
即将大范围的整数转型为小范围的整数。
12.2 浮点数运算
加减乘除
无法精确表示
浮点数运算在除数为0时,不会报错,但会返回几个特殊值:
- NaN表示Not a Number
- Infinity表示无穷大
- -Infinity表示负无穷大
double d1 = 0.0 / 0; // NaN
double d2 = 1.0 / 0; // Infinity
double d3 = -1.0 / 0; // -Infinity
比较两个浮点数是否相等的方法:判断两个浮点数之差的绝对值是否小于一个很小的数
类型自动提升
参与运算的两个数其中一个是整型,那么整型可以自动提升到浮点型,但在一个复杂的四则运算中,两个整数的运算不会出现自动提升的情况
强制转型
可以将浮点数强制转型为整数。在转型时,浮点数的小数部分会被丢掉。如果转型后超过了整型能表示的最大范围,将返回整型的最大值
如果要进行四舍五入,可以对浮点数加上0.5再强制转型
package com.lxf.pro;
public class BaseDataType {
public static void main(String[] args) {
double d = 2.5;
int n = (int)(d+0.6);
System.out.println(n);
}
}
12.3 布尔运算
短路运算。如果一个布尔运算的表达式能提前确定结果,则后续的计算不再执行(&&和||)
12.4 字符串
引用类型,不可变
用+连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接
12.5 数组
引用类型,大小不可变
数组所有元素初始化为默认值,整型都是0,浮点型是0.0,布尔型是false;
12.5.1 遍历数组
- for循环
- for each循环
12.5.2 打印数组元素
- for循环
- for each循环
- java.util标准库提供的Arrays.toString()方法
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 1, 2, 3, 5, 8 };
System.out.println(Arrays.toString(ns));
}
}
12.5.3 数组排序
- 双层for循环
- java.util标准库提供的Arrays.sort()方法,该方法修改了数组本身
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] ns = { 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 };
Arrays.sort(ns);
System.out.println(Arrays.toString(ns));
}
}
12.5.4 多维数组
- 多维数组的每个数组元素的长度并不要求相同
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6 },
{ 7, 8, 9 }
};
2. 打印二维数组
java.util标准库提供的Arrays.deepToString()方法
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
System.out.println(Arrays.deepToString(ns));
}
}
12.5.5 命令行参数
命令行参数由JVM接收用户输入并传给main方法
12.6 流程控制
12.6.1 输入和输出
12.6.1.1 输出
System.out.println() 换行输出
System.out.print() 直接输出
System.out.print() 格式化输出
12.6.1.2 输入
Scanner对象
12.6.2 if判断
if … else if … 串联使用多个if时按照判断范围从大到小依次判断或从小到大依次判断
==表示“引用是否相等”
判断引用类型的变量内容是否相等,必须使用equals()方法
执行语句s1.equals(s2)时,如果变量s1为null,会报NullPointerException
package com.lxf.pro;
public class BaseDataType {
public static void main(String[] args) {
String s1 = null;
if (s1.equals("hello")) {
System.out.println("hello");
}
}
}
package com.lxf.pro;
public class BaseDataType {
public static void main(String[] args) {
String s1 = null;
//要避免NullPointerException错误,可以利用短路运算符&&
if (s1 != null && s1.equals("hello")) {
System.out.println("hello");
}
}
}
12.6.3 switch多重选择
如果有几个case语句执行的是同一组语句块,可以这么写:
public class Main {
public static void main(String[] args) {
int option = 2;
switch (option) {
case 1:
System.out.println("Selected 1");
break;
case 2:
case 3:
System.out.println("Selected 2, 3");
break;
default:
System.out.println("Not selected");
break;
}
}
}
Tip:
- 使用switch语句时,只要保证有break,case的顺序不影响程序逻辑,但是仍然建议按照自然顺序排列,便于阅读。
- switch语句还可以匹配字符串。字符串匹配时,是比较“内容相等”。
- switch语句可以使用枚举类型
- Java 12以后的新特性
switch语句升级为更简洁的表达式语法,并且不需要break语句。新语法使用->,如果有多条语句,需要用{}括起来。如果需要复杂的语句,可以写很多语句,放到{…}里,然后,用yield返回一个值作为switch语句的返回值。yield的作用类似于return
public class Main {
public static void main(String[] args) {
String fruit = "orange";
int opt = switch (fruit) {
case "apple" -> 1;
case "pear", "mango" -> 2;
default -> {
int code = fruit.hashCode();
yield code; // switch语句返回值
}
};
System.out.println("opt = " + opt);
}
}
12.6.4 for循环
Tip:
- 在for循环的循环体中,不要修改计数器的值。计数器的初始化、判断条件、每次循环后的更新条件统一放到for()语句中可以一目了然。
- 使用for循环时,计数器变量i要尽量定义在for循环中而不是循环外,如果变量i定义在for循环外,那么,退出for循环后,变量i仍然可以被访问,这就破坏了变量应该把访问范围缩到最小的原则。
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i<ns.length; i++) {
System.out.println(ns[i]);
}
// 无法访问i
int n = i; // compile error!
int[] ns = { 1, 4, 9, 16, 25 };
int i;
for (i=0; i<ns.length; i++) {
System.out.println(ns[i]);
}
// 仍然可以使用i
int n = i;
13. 面向对象
13.1 方法
13.1.1 可变参数
支持版本:Java 1.5
可变参数用类型…定义,可变参数相当于String[]数组类型,与数组不同的是String[]数组类型在调用时需要构造String[]
class Group {
private String[] names;
public void setNames(String[] names) {
this.names = names;
}
}
Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]
13.1.2 参数绑定
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。字符串类型的参数除外,因为字符串是不可变的。
13.1.3 private方法
仅供类内部的方法调用
13.2 属性
13.2.1 private 属性
避免外部代码直接访问属性,通过get、set方法让外部代码间接访问属性,在方法内部,可以检查参数是否合法,从而使得外部代码没有任何机会把age设置成不合理的值。
13.2.2 this变量
解决局部变量与属性名命名冲突
class Person {
private String name;
public void setName(String name) {
this.name = name; // 前面的this不可少,少了就变成局部变量name了
}
}
13.3 构造方法
解决的问题:创建对象实例时就把内部属性全部初始化为合适的值
Tip:
- 调用构造方法,必须用new操作符。如:Person p = new Person(“Xiao Ming”, 15);
- 如果自定义了一个构造方法,编译器就不再自动创建默认构造方法
- 没有在构造方法中初始化字段时,引用类型的字段默认是null,数值类型的字段用默认值,int类型默认值是0,布尔类型默认值是false
- 字段初始化有两种方法,对字段直接进行初始化和通过构造方法进行初始化,在Java中,创建对象实例的时候,按照如下顺序进行初始化:
先初始化字段,例如,int age = 10;表示字段初始化为10,double salary;表示字段默认初始化为0,String name;表示引用类型字段默认初始化为null;然后执行构造方法的代码进行初始化。 - 一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
}
public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String)
}
}
13.4 方法重载 Overload
功能类似,方法名相同,但各自的参数不同,称为方法重载。
注:方法重载的返回值类型通常都是相同的
13.5 继承
- 最重要的就是代码复用,子类继承父类的属性和方法,并在此之上进行属性和功能的扩展,通过继承,子类只需要编写额外的功能,不再需要重复代码。(子类自动获得了父类的所有字段,严禁定义与父类重名的字段!)
- 子类无法继承父类的private字段或者private方法。要想实现继承,需要将private替换为protected,protected关键字可以把字段和方法的访问权限控制在继承树内部。protected字段和方法可以被其子类,以及子类的子类所访问。
13.5.1 super关键字
应用场景:在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();如果父类的构造方法不是默认的空构造方法,子类就必须通过super关键字显式的调用父类的构造方法。
- 子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
- 子类引用父类的字段时,可以用super.fieldName。
13.5.2 Java 15 继承新特性
final
关键字修饰的类一律不准继承,而sealed
和permits
关键字限定了继承的范围,除了范围之内的其他类不可进行继承。
public sealed class Shape permits Rect, Circle, Triangle {
...
}
permits Rect, Circle, Triangle明确给出了能够从Shape类继承的子类名称。
子类可以通过下列方式进行继承
public final class Rect extends Shape {...}
注:sealed类在Java 15中目前是预览状态,要启用它,必须使用参数--enable-preview和--source 15
13.5.3 向上转型
已有继承树:Student > Person > Object
父类引用指向子类对象
Person p = new Student(); // upcasting, ok
Object o1 = new Person(); // upcasting, ok
Object o2 = new Student(); // upcasting, ok
13.5.4 向下转型
向下转型需要确定实际类型是否为子类类型,如果是子类类型就可以进行向下转型,因为子类功能多,父类功能少,功能多的可以转功能少的,而功能少的不能转功能多的
13.5.4.1 instanceof关键字
判断变量所指向的实例是否是指定类型,或者是指定类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false。
向下转型之前,通过该关键字进行判断
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}
13.5.4.2 Java 14 instanceof新特性
判断instanceof后,可以直接转型为指定变量,避免再次强制转型。
13.5.5 继承和组合
继承表示的是is关系,如:Student是Person的一种,所以Student继承Person;
组合表示的是has关系,如:Student可以持有一个Book,所以Book作为属性存在于Student中
class Student extends Person {
protected Book book;
protected int score;
}
13.6 多态
13.6.1 覆写 Override
发生在子类与父类之间,方法的方法签名相同,并且返回值也相同
@Override
可以让编译器帮助检查是否进行了正确的覆写。即以覆写的标准对方法进行检查。
13.6.2 多态
针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型
的方法。
优势:
允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
Tip:
- 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
13.6.3 final 关键字
- final修饰的方法可以阻止被覆写;
- final修饰的class可以阻止被继承;
- final修饰的field必须在创建对象时初始化,随后不可修改。
通常在构造方法中初始化final字段
//实例一旦创建,其final字段就不可修改。
class Person {
public final String name;
public Person(String name) {
this.name = name;
}
}
- final修饰局部变量可以阻止被重新赋值
package abc;
public class Hello {
protected void hi(final int t) {
t = 1; // error!
}
}
13.7 抽象类
13.7.1 应用场景
父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它
13.7.2 面向抽象编程
引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
抽象方法实际上相当于定义了“规范”。
本质
- 上层代码只定义规范(例如:abstract class Person);
- 不需要子类就可以实现业务逻辑(正常编译);
- 具体的业务逻辑由不同的子类实现,调用者并不关心。
13.8 接口
接口定义的所有方法默认都是public
、abstract
的
13.8.1 接口继承
接口继承接口使用extends
关键字,相当于扩展了接口的方法。
13.8.2 JDK 1.8 新特性
13.8.2.1 default 方法
在接口中,可以定义default方法
13.8.2.2 应用场景
如果没有default 方法,接口增加新方法时,接口的实现类就会挂掉,而通过使用default 方法,已经存在的实现类就不会挂掉,只需要在需要覆写的地方去覆写新增方法。
13.9 静态字段和静态方法
- 静态方法无法访问实例字段,只能访问静态字段和静态方法
- interface是可以有静态字段的,并且静态字段必须为
final
类型,所以接口中字段的修饰词就只能是public static final
,编写接口时,可以省略这些修饰符 - 静态方法常用于工具类和辅助方法(如:main方法)。
13.10 包
13.10.1 应用场景
防止类命名冲突
13.10.2 Tip
- 没有定义包名的类,使用的是默认包,非常容易引起名字冲突
- 源码和编译后的.class文件都按照包结构存放
13.10.3 包作用域
位于同一个包的类,可以访问包作用域
(不用public、protected、private修饰的字段和方法以及不用public、private修饰的类)的字段和方法。
13.10.4 import 关键字
13.10.4.1 应用场景
在类中便捷的引用其他类
13.10.4.2 引用方式
- 引用时写出完整类名
- 通过import引入指定的类
推荐
- import …* 引用指定包下的所有类(但不包括子包的类)
- import static:导入一个类的静态字段和静态方法
13.10.4.3 类名的查找路径
- 如果是完整类名,就直接根据完整类名查找这个class;
- 如果是简单类名,按下面的顺序依次查找:
2.1 查找当前package是否存在这个class;
2.2 查找import的包是否包含这个class;
2.3 查找java.lang包是否包含这个class。
13.10.4.4 编译器的import动作
- 自动import当前package的其他class
- 自动import java.lang.*
导入的是java.lang下的类,对于java.lang.reflect这样的子包需要单独导入
13.10.4.5 包名命名规范
- 使用倒置的域名来确保唯一性。
如:org.apache、com.liaoxuefeng.sample - 不要和java.lang包的类重名
- 不要和JDK常用类重名
13.11 作用域
- 嵌套类拥有访问private的权限
13.12 内部类
被定义在另一个类的内部
13.12.1 Inner Class
class Outer {
class Inner {
// 定义了一个Inner Class
}
}
Inner Class的实例不能单独存在,必须依附于一个Outer Class的实例。
package com.lxf.ss;
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested"); // 实例化一个Outer
Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
inner.hello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
class Inner {
void hello() {
//Inner Class除了有一个this指向它自己,还隐含地持有一个Outer Class实例,可以用Outer.this访问这个实例。
System.out.println("修改前:");
System.out.println("Hello, " + Outer.this.name);
//能访问Outer Class的private字段和方法
System.out.println("\n修改后:");
Outer.this.name = "nihao";
System.out.println("Hello, " + Outer.this.name);
}
}
}
Outer类被编译为Outer.class
,而Inner类被编译为Outer$Inner.class
。
13.12.2 Anonymous Class
定义在方法内部
Outer类被编译为Outer.class,而匿名类被编译为Outer$1.class
。如果有多个匿名类,Java编译器会将每个匿名类依次命名为Outer$1、Outer$2、Outer$3……
13.12.3 Static Nested Class
无法引用Outer.this,可以访问Outer的private静态字段和静态方法。
13.13 classpath和jar 包
classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class。
13.13.1 classpath
13.13.1.1 设定方法
- 在系统环境变量中设置classpath环境变量
不推荐
- 在启动JVM时设置classpath变量
推荐
2.1 对于java命令就是
java -classpath/-cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
2.2 对于IDE就是
当前工程的bin目录和引入的jar包。
注:没有设置系统环境变量,也没有传入-cp参数,那么JVM默认的classpath为.
13.13.2 jar包
13.13.2.1 应用场景
把package组织的目录层级,以及各个目录下的所有文件(包括.class文件和其他文件)都打成一个jar文件
13.13.2.2 特点
- jar包实际上就是一个zip格式的压缩文件
- jar包相当于目录
13.13.2.3 执行jar包
如果我们要执行一个jar包的class,就可以把jar包放到classpath中
java -cp ./hello.jar abc.xyz.Hello
13.13.2.4 创建jar包
在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip改为.jar
注:jar包的第一层目录不能是bin,而应该直接是最外层的包
13.13.2.5 简化jar包执行
在jar包的根目录下创建/META-INF/MANIFEST.MF
文件,该文件可以指定Main-Class和其它信息。JVM会自动读取这个MANIFEST.MF文件,如果存在Main-Class,我们就不必在命令行指定启动的类名,而是使用
java -jar hello.jar
直接运行jar包。
注:如果jar包包含其他jar包,就需要在MANIFEST.MF文件里配置classpath
13.14 模块
https://www.liaoxuefeng.com/wiki/1252599548343744/1281795926523938
14. Java核心类
14.1 字符串
不同版本的JDK,String类在内存中有不同的优化方式。早期JDK版本的String总是以char[]存储,较新的JDK版本的String则以byte[]存储。
14.1.1 字符串不可变性的实现原理
通过内部的private final char[]字段,以及没有任何修改char[]的方法实现的。
14.1.2 字符串内容的比较
equals()方法,若忽略大小写比较,使用equalsIgnoreCase()方法。
14.1.3 常用方法
14.1.3.1 搜索子串
方法 | 功能 |
---|---|
contains() | 是否包含子串 |
indexOf() | 第一个子串索引 |
lastIndexOf() | 最后一个子串索引 |
startsWith() | 是否以指定子串开头 |
endsWith() | 是否以指定子串结尾 |
14.1.3.2 提取子串
substring()方法
14.1.3.3 去除首尾空白字符
空白字符包括\t
,\r
,\n
,空格
空白字符串:只包含空白字符的字符出串
方法 | 功能 |
---|---|
trim() | 去除首尾空白字符,返回一个新字符串 |
strip() | 去除首尾 空白字符,包括中文空格字符\u3000 |
stripLeading() | 去除首部 空白字符,包括中文空格字符\u3000 |
stripTrailing() | 去除尾部 空白字符,包括中文空格字符\u3000 |
isEmpty() | 判断字符串是否为空 |
isBlank() | 判断字符串是否为空白字符串 |
14.1.3.4 替换子串
方法 | 功能 |
---|---|
replace() | 根据字符或字符串进行替换,返回一个新字符串 |
replaceAll() | 根据正则表达式进行替换 |
/*
根据字符或字符串进行替换
*/
String s = "hello";
s.replace('l', 'o');//heooo
s.replace("l", "w");//hewwo
/*
根据正则表达式进行替换
\, 匹配,
\; 匹配;
\s 匹配空白字符
*/
String ss = "A,,B;C ,D";
ss.replaceAll("[\\,\\;\\s]+", "s");//AsBsCsD
14.1.3.5 分割字符串
方法 | 功能 |
---|---|
split() | 通过正则表达式分割字符串 |
/*
通过正则表达式分割字符串
*/
String s = "A,B,C,D";
String[] ss = s.split("\\,");//{"A", "B", "C", "D"}
14.1.3.6 拼接字符串
方法 | 功能 |
---|---|
String.join() | 用指定的字符串连接字符串数组 |
/*
用指定的字符串连接字符串数组
*/
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
14.1.3.7 格式化字符串
方法 | 功能 |
---|---|
String.format() | 格式化字符串 |
/*
格式化字符串
*/
String.format("Hi %s, your score is %.2f!", "Bob", 59.5);//Hi Bob, your score is 59.50!
注:参数类型要和占位符一致,%s可以显示任何数据类型.
常用占位符
占位符 | 功能 |
---|---|
%s | 显示字符串,可以显示任何数据类型 |
%d | 显示整数 |
%x | 显示十六进制整数 |
%f | 显示浮点数 |
14.1.4 类型转换
14.1.4.1 基本类型或引用类型转换为字符串
String.valueOf()方法
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
14.1.4.2 字符数组转换为字符串
char [] cs = {'a','b','c'};
String s = new String(cs); // char[] -> String
System.out.println(s);//abc
14.1.4.3 字符串转换为int类型
Integer.parseInt() 方法
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
容易被误解的方法:Integer.getInteger()
把字符串对应的系统变量转换为Integer
14.1.4.4 字符串转换为boolean类型
Boolean.parseBoolean()方法
boolean b1 = Boolean.parseBoolean("true"); // true
14.1.4.5 字符串转换为字符数组
char[] cs = "Hello".toCharArray(); // String -> char[]
for (char c: cs) {
System.out.print(c);//Hello
}
14.2 字符编码
14.2.1 常见编码形式
编码 | 编码范围 | 制定组织 | 占用空间 | 特性 | 编码示例 |
---|---|---|---|---|---|
ASCII | 英文字母、数字和常用符号 | ANSI | 1字节 | 最高位始终为0 | ‘A’(0x41) |
GB2312 | 中文 、英文字母、数字和常用符号 | 2字节 | 最高位始终为1 | ‘中’(0xd6d0) | |
Shift_JIS | 日文 、英文字母、数字和常用符号 | ||||
EUC-KR | 韩文 、英文字母、数字和常用符号 | ||||
Unicode | 统一全球所有语言的编码 | 全球统一码联盟 | 两个或者更多字节 | 英文字符的Unicode编码就是简单地在ASCII编码前面添加一个00字节 | ‘A’(0x0041) |
UTF-8 | 1~4字节的变长编码 | 依靠高字节位来确定一个字符究竟是几个字节 | ‘A’(0x41)、‘中’(0xe4b8ad) |
14.2.2 Java中的编码转换
Java的char类型就是两个字节
的Unicode
编码
14.2.2.1 字符串转换成其他编码
转换为byte[]时,优先考虑UTF-8编码
byte[] b1 = "Hello".getBytes(); // 按系统默认编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
14.2.2.2 已知编码的字节数组转换为字符串
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
14.3 StringBuilder和StringBuffer
14.3.1 应用场景
背景:
通过+
直接拼接字符串每次循环都会创建新的字符串对象,然后扔掉旧的字符串,浪费内存,影响GC效率。
StringBuilder 优点:
往StringBuilder中新增字符时,不会创建新的临时对象,还可以进行链式操作。
核心类 | 应用场景 |
---|---|
StringBuilder | 高效拼接字符串 |
StringBuffer | StringBuilder的线程安全版本很少使用 |
注:StringBuffer线程安全但执行速度慢
14.3.2 拼接字符串示例
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();
14.4 StringJoiner和String.join()
14.4.1 应用场景
核心类 | 功能 |
---|---|
StringJoiner | 用分隔符拼接数组(可以指定“开头”和“结尾”) |
String.join() | 用分隔符拼接数组 |
14.4.2 示例
StringJoiner
String[] names = {"Bob", "Alice", "Grace"};
//"Hello " 指定开头
//"!" 指定结尾
StringJoiner sj = new StringJoiner(", ", "Hello ", "!");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());//Hello Bob, Alice, Grace!
String.join()
String[] names = {"Bob", "Alice", "Grace"};
String s = String.join(", ", names);
System.out.println(s);//Bob, Alice, Grace
14.5 包装类
14.5.1 应用场景
把基本类型视为对象
14.5.2 种类
基本类型 | 包装类 |
---|---|
boolean | java.lang.Boolean |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
char | java.lang.Character |
14.5.3 Auto Boxing/Auto Unboxing(不推荐使用)
14.5.3.1 应用场景
自动装箱和自动拆箱只发生在编译阶段,面向编译器,目的是简化代码。
//Auto Boxing 自动装箱
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
//Auto Unboxing 自动拆箱
int x = n; // 编译器自动使用Integer.intValue()
14.5.3.2 特点
- 所有的包装类都是不变类
- 包装类的比较通过equals()方法比较
14.5.3.3 静态工厂方法
静态工厂方法:能创建“新”对象的静态方法
Integer包装类创建对象时,通过静态工厂方法Integer.valueOf()方法创建,它尽可能地返回缓存的实例以节省内存。
以下两种创建新对象方法的比较
//new总是创建新的Integer实例
Integer n = new Integer(100);
//尽可能地返回缓存的实例以节省内存
Integer n = Integer.valueOf(100);
14.5.4 进制转换
十进制转其他进制的整数
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
十进制转其他进制的字符串
System.out.println(Integer.toString(100)); // "100",表示为10进制
System.out.println(Integer.toString(100, 36)); // "2s",表示为36进制
System.out.println(Integer.toHexString(100)); // "64",表示为16进制
System.out.println(Integer.toOctalString(100)); // "144",表示为8进制
System.out.println(Integer.toBinaryString(100)); // "1100100",表示为2进制
14.5.5 处理无符号整型
14.6 JavaBean
14.6.1 应用场景
- 把一组数据组合成一个JavaBean便于传输
- JavaBean可以方便地被IDE工具分析,生成读写属性的代码
14.6.2 定义
字段的读写方法符合以下命名规范的类
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)
注:boolean字段比较特殊,它的读方法一般命名为isXxx()
JavaBean是一种符合命名规范的class
14.6.3 属性
一组对应的读方法(getter)和写方法(setter)称为属性(property),不一定需要对应的字段
public class Person {
private String name;
private int age;
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
public int getAge() { return this.age; }
public void setAge(int age) { this.age = age; }
public boolean isChild() {
return age <= 6;
}
}
child只读属性就不存在对应的字段。
14.6.4 枚举属性
package com.lxf.pro.string;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class StringProject {
public static void main(String[] args) throws IntrospectionException {
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println(pd.getName());
System.out.println(" " + pd.getReadMethod());
System.out.println(" " + pd.getWriteMethod());
}
}
}
class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
注:class属性是从Object继承的getClass()方法带来的
14.7 枚举类
14.7.1 应用场景
- 让编译器能自动检查某个值在枚举的集合内
- 不同用途的枚举需要不同的类型来标记,不能混用
14.7.2 特点
- enum虽然是引用类型,但enum的比较可以用
==
- 定义的enum类型总是继承自java.lang.Enum,且无法被继承
- 只能定义出enum的实例,而无法通过new操作符创建enum的实例
- 定义的每个实例都是引用类型的唯一实例
- 可以将enum类型用于switch语句
- enum的构造方法要声明为private,字段强烈建议声明为final
14.7.3 编译后的枚举类
编译前
public enum Color {
RED, GREEN, BLUE;
}
编译后
public final class Color extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}
14.7.4 方法
方法 | 功能 |
---|---|
name() | 返回常量名 |
ordinal() | 返回定义的常量的顺序,从0开始计数 |
14.7.5 自定义private构造方法
14.7.5.1 应用场景
在枚举常量和int转换时,如果通过ordinal()实现,会引起因枚举变量定义顺序不同而导致的ordinal()的返回值不同,这样,如果在其他位置引用了枚举变量,会产生逻辑错误,使得程序缺失健壮性,好的方法是通过自定义private构造方法为枚举常量添加字段,从而将int值与枚举常量进行绑定。
14.7.5.2 实现方式
package com.lxf.pro.string;
import java.beans.IntrospectionException;
public class StringProject {
public static void main(String[] args) throws IntrospectionException {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
public final int dayValue;
private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}
14.7.6 覆写toString()方法
14.7.6.1 应用场景
默认情况下,对枚举常量调用toString()会返回和name()一样的字符串。为了使枚举常量在输出时更具可读性,可对toString()方法进行覆写。
注:判断枚举常量的名字,要始终使用name()方法,绝不能调用toString()!
package com.lxf.pro.string;
import java.beans.IntrospectionException;
public class StringProject {
public static void main(String[] args) throws IntrospectionException {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Today is " + day + ". Work at home!");
} else {
System.out.println("Today is " + day + ". Work at office!");
}
}
}
enum Weekday {
MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");
public final int dayValue;
private final String chinese;
private Weekday(int dayValue, String chinese) {
this.dayValue = dayValue;
this.chinese = chinese;
}
@Override
public String toString() {
return this.chinese;
}
}
14.8 记录类
14.8.1 支持版本
Java 14
14.8.2 应用场景
用于简化Data Class(一种不变类)的定义过程
14.8.3 不变类
- 定义class时使用final,无法派生子类;
- 每个字段使用final,保证创建实例后无法修改任何字段。
- 为了保证不变类的比较,还需要正确覆写equals()和hashCode()方法,这样才能在集合类中正常使用
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return this.x;
}
public int y() {
return this.y;
}
//覆写equals()方法
//覆写hashCode()方法
//覆写toString()方法(如果有需要)
}
14.9 BigInteger
14.9.1 应用场景
Java中,由CPU原生提供的整型最大范围是64位long型整数。java.math.BigInteger就是用来表示任意大小的整数。包括超过long型范围的整数。
14.10 BigDecimal
14.10.1 应用场景
BigDecimal可以表示一个任意大小且精度完全准确的浮点数。
14.11 常用工具类
Math:数学计算
Random:生成伪随机数
SecureRandom:生成安全的随机数
15. 异常处理
15.1 异常
15.1.1 调用方获知调用失败信息的方式
- 约定返回错误码,如:返回0,表示成功,返回其他整数,表示约定的错误码,常用于c语言。
int code = processFile("C:\\test.txt");
if (code == 0) {
// ok:
} else {
// error:
switch (code) {
case 1:
// file not found:
case 2:
// no read permission:
default:
// unknown error:
}
}
- 在语言层面上提供一个异常处理机制。
15.1.2 异常体系
Error及其子类程序不可捕获
,Exception及其子类可捕获
Checked Exception(图中红色部分)必须进行捕获
RuntimeException及其子类按需进行捕获,通常不需要捕获
15.2 捕获异常
- 一个catch语句可以匹配多个非继承关系的异常。一般用于多个异常的处理逻辑相同,但异常之间不存在继承关系场景下。
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
- 如果使用try…finally结构,需要在方法上声明可能抛出的异常。
15.3 抛出异常
15.3.1 步骤
- 创建某个Exception的实例
- 用throw语句抛出
void process2(String s) {
if (s==null) {
//下面两行常缩写为
//throw new NullPointerException();
NullPointerException e = new NullPointerException();
throw e;
}
}
15.3.2 “转换”抛出的异常类型
如果一个方法捕获了某个异常后,又在catch子句中抛出新的异常,就相当于把抛出的异常类型“转换”了
package com.lxf.ss;
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
try {
process2();
} catch (NullPointerException e) {
//process1检测到process2抛出的NullPointerException异常后重新抛出了IllegalArgumentException,导致第一现场的信息丢失了
throw new IllegalArgumentException();
}
}
static void process2() {
throw new NullPointerException();
}
}
注:直接抛出新的异常会导致第一现场的信息丢失
解决方法:
package com.lxf.ss;
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
try {
process2();
} catch (NullPointerException e) {
//抛出新异常时传入捕获到的异常,新的Exception就可以持有原始Exception信息,从而能够定位到第一现场的信息。
throw new IllegalArgumentException(e);
}
}
static void process2() {
throw new NullPointerException();
}
}
注: 捕获到异常并再次抛出时,一定要留住原始异常
15.3.3 仅在catch中抛出异常
public class Main {
public static void main(String[] args) {
try {
Integer.parseInt("abc");
} catch (Exception e) {
System.out.println("catched");
//异常会在执行完finally块之后抛出
throw new RuntimeException(e);
} finally {
System.out.println("finally");
}
}
}
15.3.4 在catch和finally中均抛出异常
public class Main {
public static void main(String[] args) {
try {
Integer.parseInt("abc");
} catch (Exception e) {
System.out.println("catched");
//RuntimeException被屏蔽了
throw new RuntimeException(e);
} finally {
System.out.println("finally");
throw new IllegalArgumentException();
}
}
}
抛出了finally块中的异常,catch块中的异常被屏蔽了,该未抛出的异常被称为Suppressed Exception
程序执行过程中只能抛出一个异常,所以,finally块中的异常抛出后,catch块中的异常就被屏蔽了
获知所有异常的方法如下:
public class Main {
public static void main(String[] args) throws Exception {
Exception origin = null;
try {
System.out.println(Integer.parseInt("abc"));
} catch (Exception e) {
//1.用origin变量保存原始异常
origin = e;
throw e;
} finally {
Exception e = new IllegalArgumentException();
if (origin != null) {
//2.添加原始异常到现有异常中
e.addSuppressed(origin);
}
//3.抛出现有异常
throw e;
}
}
}
注:Throwable.getSuppressed()可以获取所有的Suppressed Exception。
15.4 自定义异常
- 抛出异常时,尽量复用JDK已定义的异常类型
- 自定义异常体系时,推荐从RuntimeException派生“根异常”,再派生出业务异常
- 自定义异常时,应该提供多种构造方法(实际上就是RuntimeException的构造方法),可以通过IDE根据父类快速生成子类的构造方法
15.5 NullPointerException(NPE)
通常由JVM抛出
15.5.1 处理NullPointerException
- 不能使用catch隐藏该错误
// 错误示例: 捕获NullPointerException
try {
transferMoney(from, to, amount);
} catch (NullPointerException e) {
//捕获后什么处理措施都没有,该错误就被隐藏了
}
- 早暴露,早修复
15.5.2 预防产生NullPointerException
- 成员变量在定义时初始化使用空字符串""而不是默认的null
- 方法返回空字符串""、空数组而不是null,这样调用方不需要检查结果是否为null,如果调用方一定要根据null判断,比如返回null表示文件不存在,那么考虑返回
Optional<T>
,这样调用方必须通过Optional.isPresent()判断是否有结果。
public Optional<String> readFromFile(String file) {
if (!fileExist(file)) {
return Optional.empty();
}
...
}
15.5.3 定位NullPointerException(Java 14)
报错时JVM可以给出详细的信息告诉我们null对象到底是谁
默认关闭,通过-XX:+ShowCodeDetailsInExceptionMessages
参数开启
java -XX:+ShowCodeDetailsInExceptionMessages Main.java
15.6 断言【很少使用】
15.6.1 应用场景
开发和测试阶段调试使用,断言失败时抛出AssertionError,程序结束退出;断言成功掠过断言语句,继续执行程序
15.6.2 添加断言消息
15.6.3 启用断言
JVM默认关闭断言指令,遇到assert语句自动忽略,不执行
//-ea:-enableassertions
java -ea Main.java
//对特定类启用断言
-ea:com.itranswarp.sample.Main
//对特定包启用断言
-ea:com.itranswarp.sample...
15.7 JDK Logging【很少使用】
java.util.logging
15.7.1 应用场景
取代System.out.println()调试方式
15.7.2 日志级别
JDK的Logging定义了7个日志级别,从严重到普通:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
默认级别是INFO,INFO以下的日志,不会被打印出来,如FINE级别的日志
15.7.3 局限性
- Logging系统在JVM启动时读取配置文件并完成初始化,一旦开始运行main()方法,就无法修改日志配置
- 配置不方便,需要在JVM启动时传递参数-Djava.util.logging.config.file=
<config-file-name>
15.8 Commons Logging
org.apache.commons.logging,第三方日志库,Apache创建的日志模块
15.8.1 应用场景
可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
15.8.2 日志级别
- FATAL
- ERROR
- WARNING
- INFO
- DEBUG
- TRACE
默认级别是INFO,INFO以下的日志,不会被打印出来,如DEBUG级别的日志
15.8.3 使用
注:使用需要下载第三方jar包
- 通过LogFactory获取Log类的实例;
- 使用Log实例的方法打日志。
15.8.3.1 静态方法中引用Log
直接定义一个静态类型变量
// 在静态方法中引用Log:
public class Main {
static final Log log = LogFactory.getLog(Main.class);
static void foo() {
log.info("foo");
}
}
15.8.3.2 实例方法中引用Log
定义一个实例变量
// 在实例方法中引用Log:
public class Person {
protected final Log log = LogFactory.getLog(getClass());
void foo() {
log.info("foo");
}
}
注:LogFactory.getLog(getClass())也可以用LogFactory.getLog(Person.class),前者的好处是子类可以直接使用父类的log实例
15.8.4 日志方法的增强
Commons Logging的日志方法,例如info(),除了标准的info(String)外,还提供了一个非常有用的重载方法:info(String, Throwable),使得记录异常更加简单,通过
try {
...
} catch (Exception e) {
log.error("got exception!", e);
}
实现。
15.9 Log4j
15.9.1 特点
- Log4j可以把同一条日志输出到不同的目的地
- 在输出日志的过程中,通过Filter来过滤哪些log需要被输出,哪些log不需要被输出。例如,仅输出ERROR级别的日志。
- 通过Layout来格式化日志信息,例如,自动添加日期、时间、方法名称等信息。
- 配合Commons Logging一起使用
- 通过配置文件进行配置
15.9.2 使用
注:使用需要下载第三方jar包
与Log4j相关的jar包
- log4j-api-2.x.jar
- log4j-core-2.x.jar
- log4j-jcl-2.x.jar
15.10 SLF4J和Logback
Commons Logging和Log4j的升级版,SLF4J对应Commons Logging,Logback对应Log4j
16. 反射
程序在运行期可以拿到一个对象的所有信息。即通过Class实例获取class信息
16.1 Class类
16.1.1 应用场景
反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
16.1.2 关于Class类
- 每加载一种class,JVM就为其创建一个Class类型的实例,并关联起来。
- Class类的构造方法是private,只有JVM能创建Class实例
- 一个Class实例包含了该class的所有完整信息
- 获取到某个Class对象时,实际上就获取到了一个类的类型
16.1.2 获取class的Class实例的方式
- 通过class的静态变量class获取
- 通过实例变量提供的getClass()方法获取
- 通过静态方法Class.forName(完整类名)获取
注:上述方法获取的Class实例是同一个实例。可以用==比较两个Class实例
16.1.3 通过Class实例来创建对应类型的实例
// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
//(String) cls.newInstance() 相当于 new String()
String s = (String) cls.newInstance();
注:与new String()不同,只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用。
16.1.4 几种特殊的Class
- 数组(如String[])也是一种Class,类名是
[Ljava.lang.String
- JVM为每一种基本类型如int也创建了Class
16.1.5 instanceof和==
== 精确类型比较
instanceof不但匹配指定类型,还匹配指定类型的子类
16.1.6 动态加载
JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载。可以在运行期根据条件来控制加载class
。
16.2 访问字段
通过反射读写字段是一种非常规方法,它会破坏对象的封装。
16.2.1 获取字段
Class提供了几个方法来获取字段
方法 | 功能 |
---|---|
Field getField(name) | 根据字段名获取某个public的field(包括父类) |
Field getDeclaredField(name) | 根据字段名获取当前类的某个field(不包括父类) |
Field[] getFields() | 获取所有public的field(包括父类) |
Field[] getDeclaredFields() | 获取当前类的所有field(不包括父类) |
16.2.2 获取字段信息
Field提供了几个方法来获取字段信息(名称、类型、修饰符)
方法 | 功能 |
---|---|
getName() | 返回字段名称 |
getType() | 返回字段类型 |
getModifiers() | 返回字段的修饰符 |
get(Object) | 获取指定实例的指定字段的值 |
set(Object, Object) | 设置字段的值,第一个Object参数是指定的实例,第二个Object参数是待修改的值 |
注:通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。
16.3 访问方法
16.3.1 获取方法
Class提供了几个方法来获取方法
方法 | 功能 |
---|---|
Method getMethod(name, Class…) | 获取某个public的Method(包括父类) |
Method getDeclaredMethod(name, Class…) | 获取当前类的某个Method(不包括父类) |
Method[] getMethods() | 获取所有public的Method(包括父类) |
Method[] getDeclaredMethods() | 获取当前类的所有Method(不包括父类) |
16.3.2 获取方法信息
方法 | 功能 |
---|---|
getName() | 返回方法名称 |
getReturnType() | 返回方法返回值类型,也是一个Class实例 |
getParameterTypes() | 返回方法的参数类型,是一个Class数组 |
getModifiers() | 返回方法的修饰符,它是一个int,不同的bit表示不同的含义。 |
注:通过Method实例可以访问某个类的方法,如果存在访问限制,要首先调用setAccessible(true)来访问非public方法。
16.3.3 调用方法
方法 | 功能 |
---|---|
invoke(Method实例, 实例方法的参数列表…) | 调用实例方法 |
invoke(null, 静态方法的参数列表…) | 调用静态方法 |
16.3.3.1 调用实例方法
package com.lxf.ss;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// String对象:
String s = "Hello world";
// 获取String substring(int)方法,参数为int:
Method m = String.class.getMethod("substring", int.class, int.class);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6, 8);
// 打印调用结果:
System.out.println(r);
}
}
16.3.3.2 多态
使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。
Method m = Person.class.getMethod("hello");
m.invoke(new Student());
相当于
Person p = new Student();
p.hello();
16.4 调用构造方法
16.4.1 实现方式
- Class提供的newInstance()方法
只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。 - 通过Constructor实例创建对象
可以调用任意的构造方法
16.4.2 获取Constructor
方法 | 功能 |
---|---|
getConstructor(Class…) | 获取某个public的Constructor |
getDeclaredConstructor(Class…) | 获取某个Constructor |
getConstructors() | 获取所有public的Constructor |
getDeclaredConstructors() | 获取所有Constructor |
注:调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。
16.4.3 示例
package com.lxf.ss;
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
// 获取构造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 通过Constructor实例调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);
}
}
16.5 获取继承关系
方法 | 功能 |
---|---|
getSuperclass() | 获取当前类的父类的Class |
getInterfaces() | 获取当前类直接实现的接口类型,如果一个类没有实现任何interface,那么getInterfaces()返回空数组。 |
注:getInterfaces()只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型
16.5.1 instanceof和isAssignableFrom()
方法 | 功能 |
---|---|
instanceof | 判断一个实例是否是某个类型 |
isAssignableFrom() | 两个Class实例,判断一个向上转型是否成立 |
16.6 动态代理
JDK提供的动态创建接口对象的方式,就叫动态代理。
16.6.1 应用场景
不编写实现类,直接在运行期动态创建某个interface的实例
16.6.2 步骤
- 定义一个InvocationHandler实例,它负责实现接口的方法调用;
- 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
2.1 使用的ClassLoader,通常就是接口类的ClassLoader;
2.2 需要实现的接口数组,至少需要传入一个接口进去;
2.3 用来处理接口方法调用的InvocationHandler实例。 - 将返回的Object强制转型为接口。
17. 注解
放在Java源码的类
、方法
、字段
、参数
前的一种特殊“注释”
17.1 使用注解
17.1.1 注解与注释
- 注释会被编译器直接忽略
- 注解可以被编译器打包进入class文件
17.1.2 分类
17.1.2.1 由编译器使用的注解
这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。
17.1.2.2 由工具处理.class文件使用的注解
这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。
17.1.2.3 程序运行期能够读取的注解
在加载后一直存在于JVM中,是最常用的注解。
17.1.3 配置参数
17.2 定义注解
17.2.1 格式
public @interface Report {
//注解的参数类似无参数方法
int type() default 0;
String level() default "info";
String value() default "";
}
注:default用于设定默认值,最常用的参数应当命名为value
17.2.2 元注解
可以修饰其他注解的注解被称为元注解
注:通常只需要使用Java标准库提供的元注解,不需要自己去编写元注解
17.2.2.1 @Target
定义注解能够被应用于源码的哪些位置
- 应用于类或接口:ElementType.TYPE;
- 应用于字段:ElementType.FIELD;
- 应用于方法:ElementType.METHOD;
- 应用于构造方法:ElementType.CONSTRUCTOR;
- 应用于方法参数:ElementType.PARAMETER。
注:如果想要指定多个位置,需要把@Target注解参数变为数组,如@Target({ElementType.METHOD,ElementType.FIELD})
17.2.2.2 @Retention
定义了Annotation的生命周期
- 仅编译期:RetentionPolicy.SOURCE;
- 仅class文件:RetentionPolicy.CLASS;
- 运行期:RetentionPolicy.RUNTIME。
注:如果@Retention不存在,则该Annotation默认为CLASS,自定义注解务必要加上@Retention(RetentionPolicy.RUNTIME)
17.2.2.3 @Repeatable【不常用】
定义Annotation是否可重复,经过@Repeatable修饰后,在某个类型声明处,就可以添加多个@Report注解
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
17.2.2.4 @Inherited
定义子类是否可继承父类定义的Annotation
注:@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效
17.2.3 步骤
- 用@interface定义注解
- 添加参数、默认值
把最常用的参数定义为value(),推荐所有参数都尽量设置默认值。 - 用元注解配置注解
必须设置@Target和@Retention,@Retention一般设置为RUNTIME,以便在运行期读取该注解,一般情况下,不必写@Inherited和@Repeatable
17.3 处理注解
17.3.1 关于注解
SOURCE类型的注解主要由编译器使用,只使用,不编写
;CLASS类型的注解主要由底层工具库使用,很少使用
;RUNTIME类型的注解,经常使用,经常编写
17.3.2 读取RUNTIME类型的注解
使用反射API读取Annotation
方法 | 功能 |
---|---|
Class.isAnnotationPresent(Class) | 判断某个注解是否存在于Class |
Field.isAnnotationPresent(Class) | 判断某个注解是否存在于Field |
Method.isAnnotationPresent(Class) | 判断某个注解是否存在于Method |
Constructor.isAnnotationPresent(Class) | 判断某个注解是否存在于Constructor |
Class.getAnnotation(Class) | 读取Class注解 |
Field.getAnnotation(Class) | 读取Field注解 |
Method.getAnnotation(Class) | 读取Method注解 |
Constructor.getAnnotation(Class) | 读取Constructor注解 |
17.3.3 读取RUNTIME类型的注解的方法
- 先判断Annotation是否存在,如果存在,就直接读取
- 直接读取Annotation,如果Annotation不存在,将返回null
17.3.4 使用注解
注解如何使用,完全由程序自己决定。定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。检查逻辑完全是我们自己编写的,JVM不会自动给注解添加任何额外的逻辑。
17.3.5 注解的应用
- 对JavaBean的属性值按规则进行检查;
18. 泛型
18.1 应用场景
编写一次,万能匹配(即编写模板代码来适应任意类型),同时通过编译器保证了类型安全
18.2 向上转型
ArrayList<T>
可以向上转型为List<T>
,但不能把ArrayList<Integer>
向上转型为ArrayList<Number>
或List<Number>
18.3 使用泛型
18.3.1 ArrayList<T>
泛型就是定义一种模板,例如ArrayList,然后在代码中为用到的类创建对应的ArrayList<类型>
- 使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object
- 泛型可以进行简写,编译器能自动推断
// 可以省略后面的Number,编译器可以自动推断泛型类型:
List<Number> list = new ArrayList<>();
18.3.2 泛型接口(在接口中使用泛型)
- 可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。
- Arrays.sort(Object[]),可以对任意数组进行排序,但待排序的元素必须实现
Comparable<T>
这个泛型接口
18.4 编写泛型
18.4.1 步骤
- 按照某种类型来编写类
- 标记所有的特定类型
- 把特定类型String替换为T,并申明<T>
18.4.2 静态方法
- 泛型类型<T>不能用于静态方法,无法在静态方法的方法参数和返回类型上使用泛型类型T
- 静态方法使用泛型类型,需要单独改写为“泛型”方法,需要使用另一个类型<K>
18.4.3 多个泛型类型
不总是存储两个类型一样的对象,就可以使用类型<T, K>
如Map<K, V>
18.5 擦拭法
不同语言的泛型实现方式不一定相同,Java语言的泛型实现方式是擦拭法
即虚拟机对泛型其实一无所知,所有的工作都是编译器做的。
18.5.1 特点
- 编译器把类型<T>视为Object;
- 编译器根据<T>实现安全的强制转型。
- Java的泛型是由编译器在编译时实行的
- 编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。
- 编译器会阻止一个实际上会变成覆写的泛型方法定义
18.5.2 局限
- <T>不能是基本类型
- 无法取得带泛型的Class(所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair<Object>。)
- 无法判断带泛型的类型
- 不能实例化T类型,实例化T类型,须借助额外的Class<T>参数
18.5.3 泛型继承
一个类可以继承自一个泛型类。
public class IntPair extends Pair<Integer> {
}
在继承了泛型类型的情况下,子类可以获取父类的泛型类型<T>。
19. 集合
19.1 简介
可以在内部持有若干其他Java对象,并对外提供访问接口的对象称为集合。
19.1.1 特点
- 接口和实现类相分离
- 支持泛型,可以限制在一个集合中只能放入同一种数据类型的元素
- 访问集合通过
迭代器
来实现,这种方式的好处是无需知道集合内部元素是按什么方式存储的
19.1.2 所在位置
java.util
19.1.3 遗留类和接口
尽量不要使用遗留类和接口
遗留类
Hashtable:一种线程安全的Map实现;
Vector:一种线程安全的List实现;
Stack:基于Vector实现的LIFO的栈。
遗留接口
Enumeration<E>:已被Iterator<E>取代。
19.1.4 分类
List,Set和Map
注:数组也是集合
19.2 使用List
实现类:ArrayList和LinkedList
19.2.1 ArrayList
数组实现
当数组已满,继续添加元素时,ArrayList先创建一个更大的新数组,然后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组
19.2.2 LinkedList
链表实现
19.2.3 ArrayList和LinkedList的比较
比较点 | ArrayList | LinkedList |
---|---|---|
获取指定元素 | 速度很快 | 需要从头开始查找元素 |
添加元素到末尾 | 速度很快 | 速度很快 |
在指定位置添加/删除 | 需要移动元素 | 不需要移动元素 |
内存占用 | 少 | 较大 |
注:优先使用ArrayList
19.2.4 特点
- List允许添加重复的元素
- List允许添加null
19.2.5 使用
19.2.5.1 创建List
- 使用ArrayList,List<String> list = new ArrayList<>();
- 使用LinkedList,List<String> list = new LinkedList<>();
- 通过List接口提供的of()方法,根据给定元素快速创建【java 9可用】
List<Integer> list = List.of(1, 2, 5);
注:List.of()方法不接受null值
19.2.5.2 遍历List
- for循环根据索引配合get(int)方法
不推荐,get(int)方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢。 - 迭代器Iterator
通过Iterator遍历List永远是最高效的方式
List<String> list = new ArrayList<>();
list.add(null); // size=2
list.add("112");
list.add("24esf");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
- for each循环
Java编译器会自动把for each循环变成Iterator的调用
19.2.5.3 List和Array转换
19.2.5.3.1 List -》Array
- 调用List的toArray()方法
返回Object[]数组,会丢失类型信息,少用 - 给toArray(T[])传入一个类型相同的Array,List内部自动把元素复制到传入的Array中
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add(null); // size=2
list.add("112");
list.add("24esf");
String[] array = list.toArray(new String[3]);
//如果传入的数组size小于List实际长度,List自动创建一个与实际List长度相同的数组,填充后返回。
//String[] array = list.toArray(new String[1]);
//如果传入的数组size大于List实际长度,填充完元素后,剩下的数组元素填充为null
//String[] array = list.toArray(new String[6]);
//最常用的是传入一个恰好大小的数组。
//String[] array = list.toArray(new String[list.size()]);
for (String n : array) {
System.out.println(n);
}
System.out.println(array.length);
}
}
- 还有一种简洁的函数式写法
String[] array = list.toArray(String[]::new);
19.2.5.3.2 Array -》List
- List.of(T…) 【Java 9】
返回的是一个只读List - Arrays.asList(T…)
返回的是一个只读List
Integer[] array = { 1, 2, 3 };
List<Integer> list = Arrays.asList(array);
for (Integer x:list) {
System.out.println(x);
}
19.3 编写equals方法
List的contains()、indexOf()方法,不是通过==
判断两个元素是否相等,而是使用equals()方法判断两个元素是否相等
package com.lxf.fx;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Person> list = new ArrayList<>();
list.add(new Person("Xiao Ming"));
list.add(new Person("Xiao Hong"));
list.add(new Person("Bob"));
System.out.println(list.contains(new Person("Bob"))); // false
}
}
class Person {
String name;
public Person(String name) {
this.name = name;
}
}
原因就是没有覆写equals()方法,默认使用Object对象的equals()方法
该方法用于判断地址是否相对,引用到上面的例子,就是循环遍历List中的元素,调用new Person(“Bob”)的equals()方法,传入元素,比较List中是否存在地址与new Person(“Bob”)地址相同的元素,故返回false
19.3.1 步骤
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false;
- 对引用类型用Objects.equals()比较,对基本类型直接用==比较。
注:如果不调用List的contains()、indexOf()这些方法,那么放入的元素就不需要实现equals()方法。即如果不在List中查找元素,就不必覆写equals()方法。
19.4 使用Map
19.4.1 应用场景
通过一个键去查询对应的值,如在List中根据name查找某个指定的Student的分数,使用List来实现存在效率非常低的问题。
19.4.2 实现类
最常用的实现类是HashMap
19.4.3 特点
- 通过key获取value,如果key不存在,get()方法返回null
- 一个key只能关联一个value,放入相同的key,只会把原有的key-value对应的value给替换掉,put()方法会返回被删除的旧的value,否则,返回null。
- Map中,虽然key不能重复,但value是可以重复的
- Map不保证顺序
19.4.4 遍历Map
- 遍历key:使用for each循环遍历Map实例的keySet()方法返回的Set集合
- 同时遍历key和value:使用for each循环遍历Map对象的entrySet()集合
19.5 编写equals和hashCode
19.5.1 Map模型
数组实现
19.5.2 Map工作原理
根据键取值:首先判断给定的键和Map中的某个键是相等的,然后通过键计算出对应的索引,取到对应的值
HashMap初始化时如果不指定大小默认的数组大小只有16,如果添加的元素个数超过了16,HashMap会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度32,扩容会导致重新分布已有的key-value,因此,扩容后需要重新确定hashCode()计算的索引位置,频繁扩容对HashMap的性能影响很大。如果能够确定使用的HashMap容量,最好的方式是创建时指定容量
Map<String, Integer> map = new HashMap<>(10000);
虽然指定了10000,但HashMap内部的数组长度总是2^n,因此,实际数组长度被初始化为比10000大的16384(214)。
键值放入HashMap后,如果需要取值,通过给定键的hashCode()方法计算出存储对应value的索引,如果在存储键值时,有两个键不同(equals()),但两个键的hashCode()方法计算出的索引相同,那么,计算出索引后,还需要遍历List,因为,此时指定索引下存储的不再是简单的value,而是键值(Entry)构成的List,List中包含多个Entry
根据key直接计算出value应该存储在哪个索引
19.6 使用EnumMap
19.6.1 使用场景
作为key的对象是enum
类型
19.6.2 优点
根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode()
19.7 使用TreeMap
19.7.1 使用场景
遍历时以Key的顺序来进行排序
19.7.2 注意事项
- 放入的Key必须
实现Comparable接口
,作为Value的对象没有任何要求。 - 如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时同时指定一个自定义排序算法。
- 作为Key的class如果实现了Comparable接口,返回值中要包含0,1,-1三种值。
- TreeMap不使用equals()和hashCode(),即不需要进行覆写。
19.7.3 使用
定义作为Key的class
package com.lxf.treemap;
public class Person implements Comparable{
private String name;
private Integer score;
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int compareTo(Object o) {
Person b = (Person) o;
if (this.score.equals(b.score)){
return 0;
}
return this.score.compareTo(b.score)>0 ? 1:-1;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", score=" + score +
'}';
}
}
返回值中包含了三种情况
测试
package com.lxf.treemap;
import java.util.Map;
import java.util.TreeMap;
public class TestTreeMap2 {
public static void main(String[] args) {
Map<Person, Integer> personTreeMap = new TreeMap<>();
Person a = new Person();
a.setName("bhhh");
a.setScore(15);
Person b = new Person();
b.setName("aaaa");
b.setScore(12);
Person c = new Person();
c.setName("bhhh");
c.setScore(15);
personTreeMap.put(a, 1);
personTreeMap.put(b, 2);
for (Person d : personTreeMap.keySet()){
System.out.println(d);
}
System.out.println(personTreeMap.get(c));
}
}
19.8 使用Properties
规范
- 创建HashMap时如果能够确定容量就直接指定,防止频繁扩容带来的效率问题
- equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算;equals()中没有使用到的字段,绝不可放在hashCode()中计算。
规范
- 类中的private方法放在其他方法的后面,private方法用于类内部使用。
- 捕获了异常需要进行处理,最简单的处理方法是调用printStackTrace()打印异常栈。
- 多个catch的顺序,子类放在前面,多个catch语句只有一个能被执行
- 捕获异常并再次抛出新的异常时,应该持有原始异常信息
- 提问时异常信息要包含最关键的
Caused by: xxx
- 通常不要在finally中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。
FAQ
1. 三元运算符
JDK版本:1.8
参考网址:https://www.liaoxuefeng.com/wiki/1252599548343744/1255938640048480
中提到的b ? x : y中x和y的类型必须相同
package com.lxf.pro;
public class BaseDataType {
public static void main(String[] args) {
System.out.println(3>2?1:'中');
}
}
2. 类的 private 引用类型字段如何确保不变性
从String的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。
现象:
package com.lxf.pro.string;
import java.util.Arrays;
public class StringProject {
public static void main(String[] args) {
int[] scores = new int[] { 88, 77, 51, 66 };
Score s = new Score(scores);
s.printScores();//[88, 77, 51, 66]
scores[2] = 99;
s.printScores();//[88, 77, 99, 66]
}
}
class Score {
private int[] scores;
public Score(int[] scores) {
this.scores = scores;
}
public void printScores() {
System.out.println(Arrays.toString(scores));
}
}
解决:
package com.lxf.pro.string;
import java.util.Arrays;
public class StringProject {
public static void main(String[] args) {
int[] scores = new int[] { 88, 77, 51, 66 };
Score s = new Score(scores);
s.printScores();//[88, 77, 51, 66]
scores[2] = 99;
s.printScores();//[88, 77, 51, 66]
}
}
class Score {
private int[] scores;
public Score(int[] scores) {
//将scores的引用直接赋给了this.scores
//this.scores = scores;
//拷贝一份赋给scores
this.scores = Arrays.copyOf(scores, scores.length);
}
public void printScores() {
System.out.println(Arrays.toString(scores));
}
}