主要参考:Java编程思想、廖雪峰官网、菜鸟教程等
1.基础
1.名词解释
Java之父:James Gosling
JDK:Java Development Kit
JRE:Java Runtime Environment
JSR规范:Java Specification Request
JCP组织:Java Community Process
为了保证Java语言的规范性,SUN供了搞了一个JSR规范,凡是想给Java平台加一个功能,比如说访问数据库的功能,大家要先创建一个JSR规范,定义好接口,这样,各个数据库厂商都按照规范写出Java驱动程序,开发者就不用担心自己写的数据库代码在MySQL上能跑,却不能跑在PostgreSQL上。
所以JSR是一系列的规范,从JVM的内存模型到Web程序接口,全部都标准化了。而负责审核JSR的组织就是JCP。
一个JSR规范发布时,为了让大家有个参考,还要同时发布一个“参考实现”,以及一个“兼容性测试套件”:
- RI:Reference Implementation
- TCK:Technology Compatibility Kit
比如有人提议要搞一个基于Java开发的消息服务器,这个提议很好啊,但是光有提议还不行,得贴出真正能跑的代码,这就是RI。如果有其他人也想开发这样一个消息服务器,如何保证这些消息服务器对开发者来说接口、功能都是相同的?所以还得提供TCK。
通常来说,RI只是一个“能跑”的正确的代码,它不追求速度,所以,如果真正要选择一个Java的消息服务器,一般是没人用RI的,大家都会选择一个有竞争力的商用或开源产品。
2.数据类型
8个基本数据类型:
- 整数类型:byte,short,int,long
- 浮点数类型:float,double
- 字符类型:char
- 布尔类型:boolean。1个字节。
所占字节数如下图:
2^7 = 128
2^16 = 65536
提问:
Java 八大基本数据类型是什么?各占多少位?
自动拆包装包什么意思?JVM底层原理是什么?
Int和byte等是怎么自动转型的?
3.数组操作
多维数组,以二维数组举例。
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(ns.length); // 3
}
}
ns包含了三个元素,每个元素又是一个数组,在内存中结构如下图,很直观:
ns[x][y]
,x就是行,y就是列。要先找到行(第一层元素),才能找到列(第二层元素)。
所以ns[1][2]
= 7。 ns.length
= 3,ns[0].length
= 4。
4.面向对象
1.classpath
classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
。
在启动JVM时设置classpath
是推荐的做法。 实际上就是给java
命令传入-classpath
或-cp
参数。如java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
没有设置系统环境变量,也没有传入-cp
参数,那么JVM默认的classpath
为.
,即当前目录。
2.模块
如果是自己开发的程序,除了一个自己的app.jar
以外,还需要一堆第三方的jar包,运行一个Java程序,一般来说,命令行写这样:
java -cp app.jar:a.jar:b.jar:c.jar com.liaoxuefeng.sample.Main
jar只是用于存放class的容器,它并不关心class之间的依赖。
从Java 9开始引入的模块,主要是为了解决“依赖”这个问题。如果a.jar
必须依赖另一个b.jar
才能运行,那我们应该给a.jar
加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar
,这种自带“依赖关系”的class容器就是模块。
为了表明Java模块化的决心,从Java 9开始,原有的Java标准库已经由一个单一巨大的rt.jar
分拆成了几十个模块,这些模块以.jmod
扩展名标识,可以在$JAVA_HOME/jmods
目录下找到它们:
- java.base.jmod
- java.compiler.jmod
- java.datatransfer.jmod
- java.desktop.jmod
- …
这些.jmod
文件每一个都是一个模块,模块名就是文件名。例如:模块java.base
对应的文件就是java.base.jmod
。模块之间的依赖关系已经被写入到模块内的module-info.class
文件了。所有的模块都直接或间接地依赖java.base
模块,只有java.base
模块不依赖任何模块,它可以被看作是“根模块”,好比所有的类都是从Object
直接或间接继承而来。
把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本。
提问:
JDK1.9的模块是什么?用来解决什么问题的?怎么用?
3.访问权限控制(略)
把方法定义为package
权限有助于测试,因为测试类和被测试类只要位于同一个package
,测试代码就可以访问被测试类的package
权限方法。
一个.java
文件只能包含一个public
类,但可以包含多个非public
类。
final
修饰符不是访问权限,它可以修饰class
、field
和method
:类不能被继承,方法不能被覆写,属性不能被二次赋值。
4.内部类(略)
5.类加载(略)
提问
类加载过程说一下?类初始化过程说一下?初始化顺序?
2.Java核心类
StringBuild:支持缓存。(线程安全?)
其他:String类等。(略)
Object类
提问:
Object类常用的方法有哪些?
getClass(),equals()、toString()、hashCode()、clone(),wait()、notify()、notifyAll(),finalize()。
String 类(略)
Java字符串的一个重要特点就是字符串不可变。实现原理:内部的private final char[]
字段,以及没有任何修改char[]
的方法。
比较相等时,注意缓存的情况:
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
// ture。true。Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池。
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
// false。true。
StringBuffer
,是Java早期的一个StringBuilder
的线程安全版本,它通过同步来保证多个线程操作StringBuffer
也是安全的,但是同步会带来执行速度的下降。
常用方法:
去除首尾空白字符
使用trim()
方法可以移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
:
" \tHello\r\n ".trim(); // "Hello"
注意:trim()
并没有改变字符串的内容,而是返回了一个新字符串。
另一个strip()
方法也可以移除字符串首尾空白字符。它和trim()
不同的是,类似中文的空格字符\u3000
也会被移除:
"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
String
还提供了isEmpty()
和isBlank()
来判断字符串是否为空和空白字符串:
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
替换子串
要在字符串中替换子串,有两种方法。一种是根据字符或字符串替换:
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
另一种是通过正则表达式替换:
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
上面的代码通过正则表达式,把匹配的子串统一替换为","
。关于正则表达式的用法我们会在后面详细讲解。
分割字符串
要分割字符串,使用split()
方法,并且传入的也是正则表达式:
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
拼接字符串
拼接字符串使用静态方法join()
,它用指定的字符串连接字符串数组:
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
格式化字符串
字符串提供了formatted()
方法和format()
静态方法,可以传入其他参数,替换占位符,然后生成新的字符串:
// String
Run
有几个占位符,后面就传入几个参数。参数类型要和占位符一致。我们经常用这个方法来格式化信息。常用的占位符有:
%s
:显示字符串;%d
:显示整数;%x
:显示十六进制整数;%f
:显示浮点数。
占位符还可以带格式,例如%.2f
表示显示两位小数。如果你不确定用啥占位符,那就始终用%s
,因为%s
可以显示任何数据类型。要查看完整的格式化语法,请参考JDK文档。
提问
String是线程安全的吗?
StringBuffer与StringBuilder什么区别?
String整体类的结构、常用方法、底层怎么实现?
3.异常处理
异常的继承结构:
Throwable
是异常体系的根,它继承自Object
。Throwable
有两个体系:Error
和Exception
。
Error
表示严重的错误,程序对此一般无能为力。如:StackOverflowError
栈溢出、OutOfMemoryError
内存耗尽、NoClassDefFoundError
无法加载类等。Exception
则是用户还可以抢救一下。
Exception
又分为两大类:
RuntimeException
以及它的子类。- 非
RuntimeException
(包括IOException
、ReflectiveOperationException
等等)
Java规定:
- 必须捕获的异常,包括
Exception
及其除RuntimeException
以外的子类,这些也叫Checked Exception。 - 不需要捕获的异常,包括
Error
及其子类,RuntimeException
及其子类。图中蓝色部分。
补充:
CheckedException
是应用程序逻辑处理的一部分,预先就可以预期到的,应该检查捕获并处理。例如:
NumberFormatException
:数值类型的格式错误FileNotFoundException
:未找到文件SocketException
:读取网络失败
RuntimeException
一般是程序逻辑编写不对造成的,应该修复程序本身。例如:
NullPointerException
:对某个null
的对象调用方法或字段IndexOutOfBoundsException
:数组索引越界
提问
简述异常的继承结构?什么是Error?什么是受检异常?什么是运行时异常?把常见的error、受检异常、运行时异常都举例说明一下。
4.反射(全)
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。
反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
大概的用法就是,想办法拿到Class对象,然后就可以新建对象、调用字段、调用方法、调用构造方法、获取继承关系等,为所欲为。另外动态代理也需要传入接口的Class对象。
1.class类
1.什么是反射?
除了int
等基本类型外,Java的其他类型全部都是class
(类似于C语言的struct?)。class
(包括interface
)的本质就是数据类型(Type
)。因为要有继承关系的数据类型才能赋值。
JVM在第一次读取到一种class
类型时,将其加载进内存,并为其创建一个Class
类型的实例,再关联起来。注意:这里的Class
类型是一个名叫Class
的class
。它长这样:
public final class Class {
private Class() {}
}
以String
类为例,当JVM加载String
类时,它首先读取String.class
文件到内存,然后,为String
类创建一个Class
实例并关联起来:
Class cls = new Class(String);
这个Class
实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class
类的构造方法是private
,只有JVM能创建Class
实例,我们自己的Java程序是无法创建Class
实例的。
所以,JVM持有的每个Class
实例都指向一个数据类型(class
或interface
),一个Class
实例包含了该class
的所有完整信息。
由于JVM为每个加载的class
创建了对应的Class
实例,并在实例中保存了该class
的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class
实例,我们就可以通过这个Class
实例获取到该实例对应的class
的所有信息。
这种通过Class
实例获取class
信息的方法称为反射(Reflection)。
2.获取class实例
总共有三种方法:
方法一:通过类,直接通过一个class
的静态变量class
获取:
Class cls = String.class;
方法二:通过对象,可以通过对象的getClass()
方法获取:
String s = "Hello";
Class cls = s.getClass();
方法三:通过完整类名,可以通过静态方法Class.forName()
获取:
Class cls = Class.forName("java.lang.String");
因为Class
实例在JVM中是唯一的,所以上述方法获取的Class
实例是同一个实例。可以用==
比较,是相等的。
3.可以从Class对象获取哪些信息?
参考如下:
static void printClassInfo(Class cls) {
System.out.println("Class name: " + cls.getName());
System.out.println("Simple name: " + cls.getSimpleName());
if (cls.getPackage() != null) {
System.out.println("Package name: " + cls.getPackage().getName());
}
System.out.println("is interface: " + cls.isInterface());
System.out.println("is enum: " + cls.isEnum());
System.out.println("is array: " + cls.isArray());
System.out.println("is primitive: " + cls.isPrimitive());
}
4.数组和基本类型的Class对象
注意到数组(例如String[]
)也是一种Class
,而且不同于String.class
,它的类名是[Ljava.lang.String
。
此外,JVM为每一种基本类型如int也创建了Class
,通过int.class
访问。
5.可以用Class对象创建新对象
如果获取到了一个Class
实例,我们就可以通过该Class
实例来创建对应类型的实例:
// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();
上述代码相当于new String()
。通过*class.newInstance()
可以创建类实例*,它的局限是:只能调用public
的无参数构造方法。带参数的构造方法,或者非public
的构造方法都无法通过class.newInstance()
被调用。
6.动态加载
JVM在第一次需要用到class时才加载,这就是JVM动态加载class
的特性。
动态加载class
的特性对于Java程序非常重要。利用JVM动态加载class
的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。利用JVM动态加载特性,大致的实现代码如下:
// Commons Logging优先使用Log4j:
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {
factory = createLog4j();
} else {
factory = createJdkLog();
}
boolean isClassPresent(String name) {
try {
Class.forName(name);
return true;
} catch (Exception e) {
return false;
}
}
这就是为什么我们只需要把Log4j的jar包放到classpath中,Commons Logging就会自动使用Log4j的原因。
小结
JVM为每个加载的class
及interface
创建了对应的Class
实例来保存class
及interface
的所有信息;
获取一个class
对应的Class
实例后,就可以获取该class
的所有信息;
通过Class实例获取class
信息的方法称为反射(Reflection);
JVM总是动态加载class
,可以在运行期根据条件来控制加载class。
2.访问字段
1.获取Feild对象
class
对象提供了以下几个方法来获取字段:
Field getField(name)
:根据字段名获取某个public
的field
(包括父类)Field[] getFields()
:获取所有public的field(包括父类)Field getDeclaredField(name)
:根据字段名获取当前类的某个field(不包括父类)Field[] getDeclaredFields()
:获取当前类的所有field(不包括父类)
一个Field
对象包含了一个字段的所有信息,用以下方法获取:
getName()
:返回字段名称,例如,"name"
;getType()
:返回字段类型,也是一个Class
实例,例如,String.class
;getModifiers()
:返回字段的修饰符,它是一个int
,不同的bit表示不同的含义。可以用Modifier.isFinal(m)、isStatic(m)、isPublic(m)、isProtected(m)、isPrivate(m)等来判断属性。
private的也能拿到。
2.获取Feild值
用feild::get(Object obj)
方法可以获取到字段的值。
遇到private属性时,可以用field::setAccessible(true)
来解锁。
不过setAccessible(true)
可能会失败。如果JVM运行期存在SecurityManager
,那么它会根据规则进行检查,有可能阻止setAccessible(true)
。例如,某个SecurityManager
可能不允许对java
和javax
开头的package
的类调用setAccessible(true)
,这样可以保证JVM核心库的安全。
使用示例:
public class Main {
public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
}
}
class Person {
private String name;
public Person(String name) { this.name = name;}
}
3.设置Feild值
通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。
设置字段值是通过Field.set(Object, Object)
实现的,其中第一个Object
参数是指定的实例,第二个Object
参数是待修改的值。示例见前面的代码f.set(p, "Xiao Hong")
。
小结
Java的反射API提供的Field
类封装了字段的所有信息:
通过Class
实例的方法可以获取Field
实例:getField()
,getFields()
,getDeclaredField()
,getDeclaredFields()
;
通过Field实例可以获取字段信息:getName()
,getType()
,getModifiers()
;
通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)
来访问非public
字段。
通过反射读写字段是一种非常规方法,它会破坏对象的封装。
3.调用方法
1.获取Method对象
用class
对象,用它的以下方法获取:
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类),Class...
为方法参数。Method[] getMethods()
:获取所有public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类),Class...
为方法参数。Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类
private的method也能拿到。
示例如下:
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public方法getScore,参数为String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的public方法getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取private方法getGrade,参数为int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
}
}
class Student extends Person {
public int getScore(String type) {
return 99;
}
private int getGrade(int year) {
return 1;
}
}
class Person {
public String getName() {
return "Person";
}
}
2.调用方法
用Method对象调用即可。即method.invoke(Object obj, Object... parems)
示例:
String s = "Hello world";
String r = s.substring(6); // "world"
// 如果用反射来调用substring方法
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);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r);
}
}
3.调用静态方法
如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke
方法传入的第一个参数永远为null
。我们以Integer.parseInt(String)
为例:
public class Main {
public static void main(String[] args) throws Exception {
// 获取Integer.parseInt(String)方法,参数为String:
Method m = Integer.class.getMethod("parseInt", String.class);
// 调用该静态方法并获取结果:
Integer n = (Integer) m.invoke(null, "12345");
// 打印调用结果:
System.out.println(n);
}
}
4.调用非public方法
和Field类似,设置method.setAccessible(true)
即可。
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person();
Method m = p.getClass().getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(p, "Bob");
System.out.println(p.name);
}
}
class Person {
String name;
private void setName(String name) {
this.name = name;
}
}
5.多态
多态时调用的是子类还是父类呢?使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。
如下代码:
public class Main {
public static void main(String[] args) throws Exception {
// 获取Person的hello方法:
Method h = Person.class.getMethod("hello");
// 对Student实例调用hello方法:
h.invoke(new Student());
// 以上反射代码也遵循多态原则,相当于如下:
Person p = new Student();
p.hello();
}
}
class Person {
public void hello() {
System.out.println("Person:hello");
}
}
class Student extends Person {
public void hello() {
System.out.println("Student:hello");
}
}
4.调用构造方法
我们通常使用new
操作符创建新的实例:
Person p = new Person();
如果通过反射来创建新的实例,可以调用Class提供的newInstance()方法:
Person p = Person.class.newInstance();
不过以上只能提供调用public的无参构造方法。
要获取所有构造方法,反射提供了constructor对象。
constructor对象和method对象类似,通过class对象获取方式如下:
getConstructor(Class...)
:获取某个public
的Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
注意*Constructor
总是当前类定义的构造方法,和父类无关*,因此不存在多态的问题。
同样的,调用非public
的Constructor
时,必须首先通过setAccessible(true)
设置允许访问。setAccessible(true)
可能会失败。
5.获取继承关系,即子父类的class等
1.获取class对象
三种方式,前面已经提过了:
- 用Class获取:
Class cls = String.class; // 获取到String的Class
- 用*object.getClass()*获取:
Class cls = str.getClass(); // s是String,因此获取到String的Class
- 用*Class.forName(“”)*获取:
Class s = Class.forName("java.lang.String");
三种方式获取到的class对象都是同一个,JVM里它是唯一的。
2.获取父类class:getSuperclass()
用class.getSuperclass()
获取即可。除了Object类,其他所有类都有父类。
public class Main {
public static void main(String[] args) throws Exception {
Class i = Integer.class;
Class n = i.getSuperclass(); // number
System.out.println(n);
Class o = n.getSuperclass(); // object
System.out.println(o);
System.out.println(o.getSuperclass()); // null
}
}
3.获取interface:getInterfaces()
用class.getInterfaces()
获取即可。
public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
// java.lang.Comparable
// java.lang.constant.Constable
// java.lang.constant.ConstantDesc
getInterfaces()
只返回当前类直接实现的接口类型,并不包括其父类实现的接口。
此外,对所有interface
的Class
调用getSuperclass()
返回的是null
,获取接口的父接口要用getInterfaces()
:
System.out.println(java.io.DataInputStream.class.getSuperclass()); // java.io.FilterInputStream,因为DataInputStream继承自FilterInputStream
System.out.println(java.io.Closeable.class.getSuperclass()); // null,对接口调用getSuperclass()总是返回null,获取接口的父接口要用getInterfaces()
如果一个类没有实现任何interface
,那么getInterfaces()
返回空数组。
4.获取继承关系:isAssignableFrom()
当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof
操作符:
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true
如果是两个Class
实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()
:
// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
6.动态代理
JDK提供了一个Proxy.newProxyInstance()
来在运行期才创建了一个接口的对象。
一个最简单的动态代理实现如下:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
在运行期动态创建一个interface
实例的方法如下:
- 定义一个
InvocationHandler
实例,它负责实现接口的方法调用; - 通过*
Proxy.newProxyInstance()
创建interface
实例*,它需要3个参数:- 类加载器:使用的
ClassLoader
,通常就是接口类的ClassLoader
; - 接口:需要实现的接口数组,至少需要传入一个接口进去;
- InvocationHandler:用来处理接口方法调用的
InvocationHandler
实例。
- 类加载器:使用的
- 将返回的
Object
强制转型为接口。
动态代理实际上是JVM在运行期动态创建class字节码并加载的过程,把上面的动态代理改写为静态实现类大概长这样:
public class HelloDynamicProxy implements Hello {
InvocationHandler handler;
public HelloDynamicProxy(InvocationHandler handler) {
this.handler = handler;
}
public void morning(String name) {
handler.invoke(
this,
Hello.class.getMethod("morning", String.class),
new Object[] { name });
}
}
其实就是JVM帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码)。
提问:
反射是什么?是用来解决什么问题的?简要说说反射的原理(无非就是玩class)?
反射就是在运行期获取对象信息的行为。用来解决对某个对象一无所知时,怎么使用它的问题。原理大概就是,JVM在加载class文件时,会对应的生成唯一的Class对象,它包含了这个class的所有信息。所以我们拿到Class对象,就获取到了某未知对象的所有信息。
反射和动态代理什么关系?动态代理是jdk1.3加到反射包里的,用来在运行时动态的生成接口的对象。
反射怎么获取feild对象?怎么获取值?
反射怎么获取method对象?怎么使用?
反射怎么获取constructor对象?怎么使用?
反射怎么获取父类class对象?如何判断两个class对象的继承关系?
动态代理怎么实现?和反射有什么联系?
5.注解(全)
注解可以被编译器打包进入class文件,是一种用作标注的“元数据”。
1.元注解
有以下4个元注解:
- @Target:生效位置:类/接口、字段、方法、构造方法、方法参数。
- @Retention:生命周期:仅编译期、class文件、运行期
- @Repeatable:是否可重复。
- @Inherited:子类是否可继承父类的注解。仅针对
@Target(ElementType.TYPE)
类型的注解有效,且仅针对class
的继承,对interface
的继承无效。
下面进行详细介绍:
1.@Target:生效位置
定义Annotation
能够被应用于源码的哪些位置:
- 类或接口:
ElementType.TYPE
; - 字段:
ElementType.FIELD
; - 方法:
ElementType.METHOD
; - 构造方法:
ElementType.CONSTRUCTOR
; - 方法参数:
ElementType.PARAMETER
。
// 定义注解@Report可用在方法或字段上,可以把@Target注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }:
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}
2.@Retention:生命周期
定义了Annotation
的生命周期:
- 仅编译期:
RetentionPolicy.SOURCE
; - 仅class文件:
RetentionPolicy.CLASS
; - 运行期:
RetentionPolicy.RUNTIME
。
如果@Retention
不存在,则该Annotation
默认为CLASS
。因为通常我们自定义的Annotation
都是RUNTIME
。
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
3.@Repeatable:注解是否可重复
可以定义Annotation
是否可重复。这个注解应用不是特别广泛。
@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Target(ElementType.TYPE)
public @interface Reports {
Report[] value();
}
// 经过@Repeatable修饰后,在某个类型声明处,就可以添加多个@Report注解:
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
4.@Inherited:是否可继承
@Inherited
仅针对@Target(ElementType.TYPE)
类型的annotation
有效,并且仅针对class
的继承,对interface
的继承无效:
@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
// 在使用的时候,如果一个类用到了@Report:
@Report(type=1)
public class Person {
}
// 则它的子类默认也定义了该注解:
public class Student extends Person {
}
自定义注解定义示例:
必须设置@Target
和@Retention
,@Retention
一般设置为RUNTIME
,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写@Inherited
和@Repeatable
:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
2.处理注解
1.读取注解信息
这里只讨论如何读取RUNTIME
类型的注解。
(1)读取class的注解:
Java提供的使用反射API读取Annotation
的方法包括:
*判断某个注解是否存在于Class
、Field
、Method
或Constructor
*:
class.isAnnotationPresent(Class class)
field.isAnnotationPresent(Class class)
method.isAnnotationPresent(Class class)
constructor.isAnnotationPresent(Class class)
// 例:判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);
使用反射API读取annotation对象:
class.getAnnotation(Class annotationClass)
field.getAnnotation(Class annotationClass)
method.getAnnotation(Class annotationClass)
constructor.getAnnotation(Class annotationClass)
例如:
// 获取Person定义的@Report注解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();
// 方法一:先判断Annotation是否存在,如果存在,就直接读取:
Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
Report report = cls.getAnnotation(Report.class);
...
}
// 方法二:直接读取Annotation,如果Annotation不存在,将返回null:
Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
...
}
(2)读取方法、字段和构造方法的Annotation
和Class类似。
(3)读取方法参数的Annotation
稍微麻烦点:因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解必须用一个二维数组来表示。
// 注解使用:
public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}
// 要读取方法参数的注解,我们先用反射获取Method实例,然后读取方法参数的所有注解:
// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range) { // @Range注解
Range r = (Range) anno;
}
if (anno instanceof NotNull) { // @NotNull注解
NotNull n = (NotNull) anno;
}
}
2.使用注解
使用示例:
(1)定义注解
// 用于字段,规定值要满足长度
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min() default 0;
int max() default 255;
}
(2)使用注解
public class Person {
@Range(min=1, max=20)
public String name;
@Range(max=10)
public String city;
}
(3)编写处理程序
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
// 遍历所有Field:
for (Field field : person.getClass().getFields()) {
// 获取Field定义的@Range:
Range range = field.getAnnotation(Range.class);
// 如果@Range存在:
if (range != null) {
// 获取Field的值:
Object value = field.get(person);
// 如果值是String:
if (value instanceof String) {
String s = (String) value;
// 判断值是否满足@Range的min/max:
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}
提问
元注解有哪些?
如何使用自定义注解?举例说明
6.泛型
1.使用泛型
示例,person列表按name排序:
// sort
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Person[] ps = new Person[] {
new Person("Bob", 61),
new Person("Alice", 88),
new Person("Lily", 75),
};
Arrays.sort(ps); // Person不实现Comparable<Person>,我们会得到ClassCastException
System.out.println(Arrays.toString(ps));
}
}
class Person implements Comparable<Person> {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
public String toString() {
return this.name + "," + this.score;
}
}
2.编写泛型
Class级别的泛型,声明在类名后面即可:
public class Pair<T, K> {
private T first;
private K second;
// ...
}
Static级别的方法不能使用类级别的泛型,可使用方法级别的泛型。
方法级别的泛型,声明在修饰符后、返回值前即可:
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... }
// 静态泛型方法应该使用其他类型区分:
public static <K> Pair<K> create(K first, K last) {
return new Pair<K>(first, last);
}
}
3.擦除法特性
Java语言的泛型实现方式是擦拭法(Type Erasure)。
擦拭法是指:虚拟机对泛型其实一无所知,所有的工作都是编译器做的。
Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T
视为Object
处理,但是,在需要转型的时候,编译器会根据T
的类型自动为我们实行安全地强制转型。
1.Java泛型局限
因此,Java泛型有以下局限:
局限一:
<T>
不能是基本类型,例如int
,因为实际类型是Object
,Object
类型无法持有基本类型。局限二:无法取得带泛型的
class
。不同类型泛型获取class
时,获取到的是同一个class
,编译后它们全部都是Pair<Object>
。观察以下代码public class Main { public static void main(String[] args) { Pair<String> p1 = new Pair<>("Hello", "world"); Pair<Integer> p2 = new Pair<>(123, 456); Class c1 = p1.getClass(); Class c2 = p2.getClass(); System.out.println(c1==c2); // true System.out.println(c1==Pair.class); // true } } class Pair<T> { private T first; private T last; public Pair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
局限三:无法判断带泛型的类型
// 并不存在Pair<String>.class,而是只有唯一的Pair.class。 Pair<Integer> p = new Pair<>(123, 456); // Compile error: if (p instanceof Pair<String>) { }
局限四:不能实例化
T
类型public class Pair<T> { private T first; private T last; public Pair() { // Compile error: first = new T(); // 相当于new Object(); last = new T(); // 相当于new Object(); } } // 要实例化T类型,我们必须借助额外的Class<T>参数: Pair<String> pair = new Pair<>(String.class);
2.有些方法不能覆写
以下方法无法通过编译:
// 因为,定义的`equals(T t)`方法实际上会被擦拭成`equals(Object t)`,而这个方法是继承自Object的,equals(T t)泛型方法的声明会误认为是方法覆写,编译器不会让通过
public class Pair<T> {
public boolean equals(T t) {
return this == t;
}
}
// 换个方法名就OK
public class Pair<T> {
public boolean same(T t) {
return this == t;
}
}
3.泛型继承
一个类可以继承自一个泛型类。
public class IntPair extends Pair<Integer> {
}
// 子类IntPair并没有泛型类型,所以,正常使用即可,不过只能用Integer类型:
IntPair ip = new IntPair(1, 2);
在父类是泛型类型的情况下,编译器会把类型T
保存到子类的class文件中。
在继承泛型类型时,子类可以获取父类的泛型类型:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class Main {
public static void main(String[] args) {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
Type firstType = types[0]; // 取第一个泛型类型
Class<?> typeClass = (Class<?>) firstType;
System.out.println(typeClass); // Integer
}
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}
4.Java的类型系统
Java引入了泛型,所以,只用Class
来标识类型已经不够了。实际上,Java的类型系统结构如下:
4.extends通配符
声明示例:
public class Pair<? extends Number> {...}
<? extends Number>
通配符的一个重要限制:方法参数签名,例如,setFirst(? extends Number)
无法传递任何Number
的子类型给setFirst(? extends Number)
。因为擦除后都是Object。
此外,**Pair\<Integer\>
不是Pair\<Number\>
的子类**。
5.super通配符
使用类似<? super Integer>
通配符作为方法参数时表示:
- 方法内部可以调用传入
Integer
引用的方法,例如:obj.setFirst(Integer n);
; - 方法内部无法调用获取
Integer
引用的方法(Object
除外),例如:Integer n = obj.getFirst();
。
即使用super
通配符表示只能写不能读。与extends
相反。只要抓住擦除法,就很好理解。
无限定通配符<?>
很少使用,可以用<T>
替换,同时它是所有<T>
类型的超类。
6.泛型和反射(待补充)
如何拿到泛型的类型?
提问
泛型是什么?用来解决什么问题?Java是用什么方式来实现泛型的?JVM底层是怎么处理的?使用泛型要注意哪些坑?
Java的Type有哪些类型?
7.集合(简略)
ArrayList
源码:
- 数据结构:使用一个数组名为
elementData
来存数据。 - 构造时不传初始数量则数组取{}。传了数量则默认改数量长度。
- 扩容时,每次扩容1.5倍:
capcity + (capacity >> 1)
。小于10则一次性扩容到10,否则扩容1.5倍。扩容时考虑Integer.max限制。扩容时机是,发现不够用的时候才会扩容。 - 扩容调用
Arrays.copyOf()
,进一步调用System.arraycopy
,底层是调用native方法,效率较高。 - 扩容是老数组复制到新数组,操作代价很高。所以尽量赋初值,避免扩容。
- 也可根据实际需求,通过调用
ensureCapacity()
方法来手动增加ArrayList实例的容量。 - 缩容:需要注意的一个点是,remove缩容后,需要对
elementData
的最后的值赋full,避免内层泄露。因为缩容是后面的元素整体向前Copy。
Fail-Fast机制:ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
LinkedList
源码:
- 数据结构:底层通过双向链表实现。
- 双向链表的每个节点用内部类Node表示。
- LinkedList通过
first
和last
引用分别指向链表的第一个和最后一个元素。 getFirst(), getLast()
:直接拿。已经缓存在变量first和last上了。为空时报错。- removeFirst():注意手动赋值
first.next
为null,以帮助GC。 - removeLast():注意手动赋值
last.prev
为null,以帮助GC。 - remove(e):删除第一次出现的这个元素,没有则返回false。判断的依据是equals。由于LinkedList可存放null元素,也可以删除null。
- remove(index):判断改下标有无元素,有的话直接
unlink()
即可。 - **
add(E e)
**:直接加到末尾。 add(int index, E element)
,该方法是在指定下表处插入元素,需要先通过线性查找找到具体位置,然后修改相关引用完成插入操作。- addAll(index, c):考虑到效率问题,不是一个一个加。而是直接在最后面继续构造链表。
- clear():为了让GC更快可以回收放置的元素,需要将node之间的引用关系赋空。
HashMap
HashSet里面有一个HashMap(适配器模式),所以底层是HashMap实现。key是元素,value全部用一个final object填充。
HashMap的key和value都可以放null。
初始容量(inital capacity,默认16)和负载系数(load factor,决定什么时候扩容,默认0.75)。
如果要将自定义的对象放入到HashMap
或HashSet
中,需要**@Override** hashCode()
和equals()
方法。
table.length
必须是2的指数:是为了hash(k)&(table.length-1)
等价于hash(k)%table.length
。提升性能。原因是HashMap要求table.length
必须是2的指数,因此table.length-1
就是二进制低位全是1,跟hash(k)
相与会将哈希值的高位全抹掉,剩下的就是余数了。
源码:
- 数据结构:数组 + 链表 + 红黑树。元素用Node内部类,有 key,value,hash 和 next 四个属性。红黑树时转为TreeNode。可根据第一个节点数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树。
get()
方法:- 找到buckets。(用当N是2的倍数时, M & (N-1) 等价 M % N ,简化了运算)。
- 判断是链表还是红黑树,遍历找到元素,根据equals判断相等,返回。
put()
方法:- 查找有没有该元素,有则直接返回。
- 如果还没有,则使用头插法,将元素插入头部。
- 插入前,会考虑是否需要扩容并重新Hash。
- 扩容方法:
remove()
方法:- 先找到
key
值对应的entry
- 删除该
entry
(修改链表的相应引用)。
- 先找到
put()方法详解:
- 数组为空?扩容,默认扩容到16。
- 第一个元素相等?先标记,最后根据入参,决定是否覆盖。返回倒是都是返回旧值。
- 第一个元素为TreeNode?调用红黑树遍历
- 插入链表最后面。插入的是第8个?链表转为红黑树。
- 链表中找到相等的Node?先标记,最后根据入参,决定是否覆盖。返回倒是都是返回旧值。
- 最后整体判断Size,看是否超过阈值threshold需要扩容。
resize()扩容方法详解:
- 获取容量
- new HashMap() 后的第一次put():取默认容量、默认阈值。
- new HashMap(int x) 后的第一次put():将阈值直接赋值给容量,此时二者相等。阈值是构造方法设置的,取第一个大于x的2的指数。
- 普通扩容:容量、阈值各 * 2。
- 扩容
- 桶内只有1个元素:直接copy。
- 桶内为红黑树:调用红黑树的split()方法分裂到新数组。
- 桶内为链表:依次重新计算桶位置,分裂为2个链表。假设老容器桶位置为j,则两个链表依次存入 j 和 capacity + j 位置。
红黑树算法?后续补充。
8.IO(简略)
五大 I/O 模型比较
- 阻塞
- 非阻塞
- I/O多路复用
- 信号驱动
- 异步
9.日期和时间(略)
10.正则(简介)
元字符:
代码 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
反义:
代码/语法 | 说明 |
---|---|
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
重复:
代码/语法 | 说明 |
---|---|
* | 前面的子串,重复0到N次 |
+ | 前面的子串,重复1到N次 |
? | 前面的子串,重复0次或1次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
再加1个:“[]”表示字符集,用于匹配多项。[a-z]表示匹配任意a-z字符
举例:
匹配邮箱:
“abcd test@runoob.com 1234”;
匹配:/\b[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,6}\b/g
11.加密(暂略)
12.函数式编程
在Java程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:
- Comparator
- Runnable
- Callable
以Comparator
为例,我们想要调用Arrays.sort()
时,可以传入一个Comparator
实例,以匿名类方式编写如下:
String[] array = ...
Arrays.sort(array, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
上述写法非常繁琐。从Java 8开始,我们可以用Lambda表达式替换单方法接口。改写上述代码如下:
// Lambda import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
System.out.println(String.join(", ", array));
}
}
1.FunctionalInterface
我们把只定义了单方法的接口称之为FunctionalInterface
,用注解@FunctionalInterface
标记。例如,Callable
接口:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
再来看Comparator
接口:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
...
}
...
}
虽然Comparator
接口有很多方法,但只有一个抽象方法int compare(T o1, T o2)
,其他的方法都是default
方法或static
方法。另外注意到boolean equals(Object obj)
是Object
定义的方法,不算在接口方法内。因此,Comparator
也是一个FunctionalInterface
。
使用Lambda表达式,我们就可以不必编写FunctionalInterface
接口的实现类,从而简化代码。
2.方法引用
实例方法、静态方法引用:
实际上,除了Lambda表达式,我们还可以直接传入方法引用。例如:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, String::compareTo); // 注意到这里的方法引用
System.out.println(String.join(", ", array));
}
}
构造方法引用:
除了可以引用静态方法和实例方法,我们还可以引用构造方法。构造方法的引用写法是类名::new
我们来看一个例子:如果要把一个List<String>
转换为List<Person>
,应该怎么办?
class Person {
String name;
public Person(String name) {
this.name = name;
}
}
List<String> names = List.of("Bob", "Alice", "Tim");
// 传统的做法是先定义一个ArrayList<Person>,然后用for循环填充这个List:
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = new ArrayList<>();
for (String name : names) {
persons.add(new Person(name));
}
// 要更简单地实现String到Person的转换,我们可以引用Person的构造方法:
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
System.out.println(persons);
}
}
class Person {
String name;
public Person(String name) {
this.name = name;
}
public String toString() {
return "Person:" + this.name;
}
}
3.使用Stream
Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream
包中。
划重点:这个Stream
代表的是任意Java对象的序列。*Stream
输出的元素可能并没有预先存储在内存中,而是实时计算出来的*。
Stream跟List稍微有点像,都是序列、都存东西,但是又完全不一样。它存的是规则,且是惰性计算的,存的东西不一定已经在内存里了。比如Stream可以存储所有的自然数,而List则不能。
Stream
的特点:
- 惰性计算:它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。
- 只转换规则:一个
Stream
可以轻易地转换为另一个Stream
,而不是修改原Stream
本身。一个Stream
转换为另一个Stream
时,实际上只存储了转换规则,并没有任何计算发生*。
因此,Stream API的基本用法就是:创建一个Stream
,然后做若干次转换,最后调用一个求值方法获取真正计算的结果:
int result = createNaturalStream() // 创建Stream
.filter(n -> n % 2 == 0) // 任意个转换
.map(n -> n * n) // 任意个转换
.limit(100) // 任意个转换
.sum(); // 最终计算结果
1.创建Stream
(1)Stream.of静态方法
创建Stream
最简单的方式,传入可变参数即可。很少用于生产,用于测试倒是很方便。
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("A", "B", "C", "D");
// forEach()方法相当于内部循环调用,
// 可传入符合Consumer接口的void accept(T t)的方法引用:
stream.forEach(System.out::println);
}
}
(2)基于数组或Collection
- 数组:
Arrays.stream()
静态方法 - Collection:直接调用Collection.stream()方法
public class Main {
public static void main(String[] args) {
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
Stream<String> stream2 = List.of("X", "Y", "Z").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
}
}
(3)基于Supplier
通过Stream.generate()
方法,需传入一个Supplier
对象。
Stream<String> s = Stream.generate(Supplier<String> sp);
基于Supplier
创建的Stream
会不断调用Supplier.get()
方法来不断产生下一个元素,这种Stream
保存的不是元素,而是算法,它可以用来表示无限序列。例如:
public class Main {
public static void main(String[] args) {
Stream<Integer> natual = Stream.generate(new NatualSupplier());
// 注意:无限序列必须先变成有限序列再打印。用到了limit(int n)方法。
natual.limit(20).forEach(System.out::println);
}
}
class NatualSupplier implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
(4)其他方法
通过一些API提供的接口,直接获得Stream
。例如:
Files
类的lines()
方法可以把一个文件变成一个Stream
,每个元素代表文件的一行内容。- 正则表达式的
Pattern
对象有一个splitAsStream()
方法,可以直接把一个长字符串分割成Stream
序列而不是数组。
(5)基本类型
Stream<>只能保存基本类型的包装类。为了提高性能,减少拆装包,Java标准库提供了IntStream
、LongStream
和DoubleStream
这三种使用基本类型的Stream
,它们的使用方法和范型Stream
没有大的区别。
// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
2.使用map
Stream.map()
是Stream
最常用的一个转换方法,它*把一个Stream
转换为另一个Stream
*。
所谓*map
操作,就是把一种操作运算,映射到一个序列的每一个元素上*。
map()
方法接收的对象是Function
接口对象,返回值是另一个stream对象。
map()
方法定义:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Function
定义:
@FunctionalInterface
public interface Function<T, R> {
// 将T类型转换为R:
R apply(T t);
}
例如,对x
计算它的平方,可以使用函数f(x) = x * x
:
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);
3.使用filter
Stream.filter()
是Stream
的另一个常用转换方法。
对一个Stream
的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream
。
例如,过滤奇数:
public class Main {
public static void main(String[] args) {
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(n -> n % 2 != 0)
.forEach(System.out::println);
}
}
4.使用reduce
map()
和filter()
都是Stream
的转换方法,而Stream.reduce()
则是Stream
的一个聚合方法,它可以把一个Stream
的所有元素按照聚合函数聚合成一个结果。
举例:从1加到10:
public class Main {
public static void main(String[] args) {
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
System.out.println(sum); // 45
}
}
reduce()
方法声明:stream.reduce(初始值,function(上次计算值,本次入参))
reduce()
方法传入的对象是BinaryOperator
接口,它定义了一个apply()
方法,负责把上次运算的结果和本次的元素进行运算,并返回运算的结果:
@FunctionalInterface
public interface BinaryOperator<T> {
// Bi操作:两个输入,一个输出
T apply(T t, T u);
}
如果去掉初始值,我们会得到一个Optional<Integer>
:
Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
// Stream的元素有可能是0个,因此返回Optional对象,需要进一步判断结果是否存在。
if (opt.isPresent()) {
System.out.println(opt.get());
}
举例,将配置文件的每一行配置通过map()
和reduce()
操作聚合成一个Map<String, String>
:
public class Main {
public static void main(String[] args) {
// 按行读取配置文件:
List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
Map<String, String> map = props.stream()
// 把k=v转换为Map[k]=v:
.map(kv -> {
String[] ss = kv.split("\\=", 2);
return Map.of(ss[0], ss[1]);
})
// 把所有Map聚合到一个Map:
.reduce(new HashMap<String, String>(), (m, kv) -> {
m.putAll(kv);
return m;
});
// 打印结果:
map.forEach((k, v) -> {
System.out.println(k + " = " + v);
});
}
}
5.输出集合
我们介绍了Stream
的几个常见操作:map()
、filter()
、reduce()
。这些操作对Stream
来说可以分为两类,
- 转换操作,即把一个
Stream
转换为另一个Stream
,例如map()
和filter()
, - 聚合操作,即对
Stream
的每个元素进行计算,得到一个确定的结果,例如reduce()
。
转换操作并不会触发任何计算,聚合方法会立刻对Stream
进行计算。
对一个Stream
做聚合计算后,结果就不是一个Stream
,而是一个其他的Java对象。
(1)输出为List
把Stream
的每个元素收集到List
的方法是调用collect()
并传入Collectors.toList()
对象。它实际上是一个Collector
实例,通过类似reduce()
的操作,把每个元素添加到一个收集器中(实际上是ArrayList
)。
类似的,collect(Collectors.toSet())
可以把Stream
的每个元素收集到Set
中。
(2)输出为数组
调用toArray()
方法,并传入数组的“构造方法”即可:
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
注意到传入的“构造方法”是String[]::new
,它的签名实际上是IntFunction<String[]>
定义的String[] apply(int)
,即传入int
参数,获得String[]
数组的返回值。
(3)输出为Map
对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
// 把元素s映射为key:
s -> s.substring(0, s.indexOf(':')),
// 把元素s映射为value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
}
(4)分组输出
分组输出使用Collectors.groupingBy()
,它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1)
,表示只要首字母相同的String
分到一组,第二个是分组的value,这里直接使用Collectors.toList()
,表示输出为List
:
public class Main {
public static void main(String[] args) {
List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
System.out.println(groups);
}
}
假设有这样一个Student
类,包含学生姓名、班级和成绩:
class Student {
int gradeId; // 年级
int classId; // 班级
String name; // 名字
int score; // 分数
}
如果我们有一个Stream<Student>
,利用分组输出,可以非常简单地按年级或班级把Student
归类。
6.其他操作
我们把Stream
提供的操作分为两类:转换操作和聚合操作。除了前面介绍的常用操作外,Stream
还提供了一系列非常有用的方法:
- 转换操作:
map()
,filter()
,sorted()
,distinct()
; - 合并操作:
concat()
,flatMap()
; - 并行处理:
parallel()
; - 聚合操作:
reduce()
,collect()
,count()
,max()
,min()
,sum()
,average()
; - 其他操作:
allMatch()
,anyMatch()
,forEach()
。
以下分别举例:
排序sorted()
对Stream
的元素进行排序十分简单,只需调用sorted()
方法,传入指定的Comparator
即可:
public class Main {
public static void main(String[] args) {
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(list);
}
}
去重distinct()
对一个Stream
的元素进行去重,没必要先转换为Set
,可以直接用distinct()
:
List.of("A", "B", "A", "C", "B", "D")
.stream()
.distinct()
.collect(Collectors.toList()); // [A, B, C, D]
截取limit()
常用于把一个无限的Stream
转换成有限的Stream
,skip()
用于跳过当前Stream
的前N个元素,limit()
用于截取当前Stream
最多前N个元素:
List.of("A", "B", "C", "D", "E", "F")
.stream()
.skip(2) // 跳过A, B
.limit(3) // 截取C, D, E
.collect(Collectors.toList()); // [C, D, E]
截取操作也是一个转换操作,将返回新的Stream
。
合并concat()
将两个Stream
合并为一个Stream
可以使用Stream
的静态方法concat()
:
Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]
flatMap
如果Stream
的元素是集合:
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));
而我们希望把上述Stream
转换为Stream<Integer>
,就可以使用flatMap()
:
Stream<Integer> i = s.flatMap(list -> list.stream());
因此,所谓flatMap()
,是指把Stream
的每个元素(这里是List
)映射为Stream
,然后合并成一个新的Stream
并行parallel()
把一个普通Stream
转换为可以并行处理的Stream
非常简单,只需要用parallel()
进行转换:
Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
.sorted() // 可以进行并行排序
.toArray(String[]::new);
经过parallel()
转换后的Stream
只要可能,就会对后续操作进行并行处理。
其他聚合方法count()
、max()
、allMatch()
除了reduce()
和collect()
外,Stream
还有一些常用的聚合方法:
count()
:用于返回元素个数;max(Comparator<? super T> cp)
:找出最大元素;min(Comparator<? super T> cp)
:找出最小元素。
针对IntStream
、LongStream
和DoubleStream
,还额外提供了以下聚合方法:
sum()
:对所有元素求和;average()
:对所有元素求平均数。
还有一些方法,用来测试Stream
的元素是否满足以下条件:
boolean allMatch(Predicate<? super T>)
:测试是否所有元素均满足测试条件;boolean anyMatch(Predicate<? super T>)
:测试是否至少有一个元素满足测试条件。
最后一个常用的方法是forEach()
,它可以循环处理Stream
的每个元素,我们经常传入System.out::println
来打印Stream
的元素:
Stream<String> s = ...
s.forEach(str -> {
System.out.println("Hello, " + str);
});
13.其他(略)
集合、IO、日期与时间、单元测试、正则、加密与安全、(多线程)、网络编程、JackSon、JDBC。
转载请注明来源