MavenMaven介绍构建插件模块mvnw网络基础TCPUDPHTTPRMIXML结构JDBC简介查询更新事务批处理连接池函数式编程基础方法引用StreamMapFilterReduce导出Stream常用方法其他
1,结构
1a-maven-project
2├── pom.xml
3├── src
4│ ├── main
5│ │ ├── java // Java源码目录
6│ │ └── resources // 资源文件的目录
7│ └── test
8│ ├── java // 测试源码
9│ └── resources // 测试资源
10└── target // 所有编译、打包生成的文件
2,pom.xml
Maven工程就是由groupId
,artifactId
和version
作为唯一标识。我们在引用其他第三方库的时候,也是通过这3个变量确定。Maven通过对jar包进行PGP签名确保任何一个jar包一经发布就无法修改。修改已发布jar包的唯一方法是发布一个新版本。因此,某个jar包一旦被Maven下载过,即可永久地安全缓存在本地。
xxxxxxxxxx
231<project ...>
2<modelVersion>4.0.0</modelVersion>
3<groupId>com.itranswarp.learnjava</groupId> // 类似于Java的包名,通常是公司或组织名称
4<artifactId>hello</artifactId> // 类似于Java的类名,通常是项目名称
5<version>1.0</version> // 版本号
6<packaging>jar</packaging>
7<properties>
8...
9</properties>
10<dependencies> // 使用<dependency>声明一个依赖后,Maven就会自动下载这个依赖包并把它放到classpath中
11<dependency>
12<groupId>commons-logging</groupId>
13<artifactId>commons-logging</artifactId>
14<version>1.2</version>
15</dependency>
16<dependency>
17<groupId>org.junit.jupiter</groupId>
18<artifactId>junit-jupiter-engine</artifactId>
19<version>5.5.2</version>
20<scope>test</scope>// 该依赖类型,不写默认为compile
21</dependency>
22</dependencies>
23</project>
3,依赖管理
当声明了自己的项目需要abc
,Maven会自动导入abc
的jar包,再判断出abc
需要xyz
,又会自动导入xyz
的jar包,不断循环自动导入所有依赖包。
Maven定义了几种依赖关系,分别是compile
、test
、runtime
和provided
:
scope | 说明 | 示例 |
---|---|---|
compile | 编译时需要用到该jar包(默认),Maven会把这种类型的依赖直接放入classpath | commons-logging |
test | 编译Test时需要用到该jar包 | junit |
runtime | 编译时不需要,但运行时需要用到 | mysql |
provided | 编译时需要用到,但运行时由JDK或某个服务器提供 | servlet-api |
Maven包含多种生命周期(lifecycle:default:构建的核心部分,编译,测试,打包,部署等,clean:在进行真正的构建之前进行一些清理工作,Site:生成项目报告,站点,发布站点),每个生命周期由一系列阶段(phase)构成,执行一个phase又会触发一个或多个goal,具体任务由一系列goal来完成,通常情况,我们总是执行phase默认绑定的goal,因此不必指定goal。。
mvn命令后面跟的是phase, maven自动根据phase确定对应的lifecycle(没有重名的phase),然后运行对应lifecycle中的第一个phase直到指定的phase
mv phase_i
:单个phase,在默认生命周期中运行到指定的phasemvn clean package
:指定多个phase,Maven先执行clean
生命周期并运行到clean
这个phase,然后执行default
生命周期并运行到package
这个phase。经常使用的命令有:
mvn clean
:清理所有生成的class和jar;
mvn clean compile
:先清理,再执行到compile
;
mvn clean test
:先清理,再执行到test
,因为执行test
前必须执行compile
,所以这里不必指定compile
;
mvn clean package
:先清理,再执行到package
。
1,Maven执行compile
这个phase,这个phase会调用compiler
插件执行关联的compiler:compile
这个goal。实际上,执行每个phase,都是通过某个插件(plugin)来执行的,Maven本身其实并不知道如何执行compile
,它只是负责找到对应的compiler
插件,然后执行默认的compiler:compile
这个goal来完成编译。所以,使用Maven,实际上就是配置好需要使用的插件,然后通过phase调用它们。
1,在软件开发中,把一个大项目分拆为多个模块是降低软件复杂度的有效方法,Maven可以有效地管理多个模块,我们只需要把每个模块当作一个独立的Maven项目,它们有各自独立的pom.xml
xxxxxxxxxx
131multiple-project
2├── pom.xml //根目录使用一个pom.xml统一编译:
3├── parent
4│ └── pom.xml //各个子模块的公共部分
5├── module-a
6│ ├── pom.xml //各个子模块部分
7│ └── src
8├── module-b
9│ ├── pom.xml //各个子模块部分
10│ └── src
11└── module-c
12 ├── pom.xml //各个子模块部分
13 └── src
parent pom.xml
x1<project xmlns="http://maven.apache.org/POM/4.0.0"
2xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4<modelVersion>4.0.0</modelVersion>
5
6<groupId>org.huangqiang.learnjava</groupId>
7<artifactId>parent</artifactId>
8<version>1.0</version>
9<packaging>pom</packaging> // <packaging>是pom而不是jar,因为parent本身不含任何Java代码。编写parent的pom.xml只是为了在各个模块中减少重复的配置
10<name>parent</name>
11
12<properties>
13<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
14<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
15<maven.compiler.source>16</maven.compiler.source>
16<maven.compiler.target>16</maven.compiler.target>
17<java.version>16</java.version>
18</properties>
19
20<dependencies>
21<dependency>
22<groupId>org.slf4j</groupId>
23<artifactId>slf4j-api</artifactId>
24<version>1.7.28</version>
25</dependency>
26<dependency>
27<groupId>ch.qos.logback</groupId>
28<artifactId>logback-classic</artifactId>
29<version>1.2.3</version>
30<scope>runtime</scope>
31</dependency>
32<dependency>
33<groupId>org.junit.jupiter</groupId>
34<artifactId>junit-jupiter-engine</artifactId>
35<version>5.5.2</version>
36<scope>test</scope>
37</dependency>
38</dependencies>
39</project>
module-a
xxxxxxxxxx
251<project xmlns="http://maven.apache.org/POM/4.0.0"
2xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4<modelVersion>4.0.0</modelVersion>
5
6<parent> //引用parent
7<groupId>org.huangqiang.learnjava</groupId>
8<artifactId>parent</artifactId>
9<version>1.0</version>
10<relativePath>../parent/pom.xml</relativePath>
11</parent>
12
13<artifactId>module-a</artifactId>
14<packaging>jar</packaging>
15<name>module-a</name>
16
17<dependencies>
18<dependency> // 如果模块间相互依赖同样可以导入相邻模块
19<groupId>org.huangqiang.learnjava</groupId>
20<artifactId>module-b</artifactId>
21<version>1.0</version>
22</dependency>
23</dependencies>
24
25</project>
在编译的时候,需要在根目录创建一个pom.xml
统一编译,在根目录执行mvn clean package
时,Maven根据根目录的pom.xml
找到包括parent
在内的共4个<module>
,一次性全部编译
xxxxxxxxxx
181<project xmlns="http://maven.apache.org/POM/4.0.0"
2xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
4
5<modelVersion>4.0.0</modelVersion>
6<groupId>org.huangqiang.learnjava</groupId>
7<artifactId>build</artifactId>
8<version>1.0</version>
9<packaging>pom</packaging>
10<name>build</name>
11
12<modules> //各个模块
13<module>parent</module>
14<module>module-a</module>
15<module>module-b</module>
16<module>module-c</module>
17</modules>
18</project>
1,使用Maven Wrapper,它可以负责给这个特定的项目安装指定版本的Maven,而其他项目不受影响。给一个项目提供一个独立的,指定版本的Maven给它使用
2,
在项目的根目录(即pom.xml
所在的目录)下:
xxxxxxxxxx
21mvn -N io.takari:maven:0.7.6:wrapper //使用最新版本的Maven
2mvn -N io.takari:maven:0.7.6:wrapper -Dmaven=3.3.3 //指定使用的Maven版本
安装后,查看项目结构,把项目的mvnw
、mvnw.cmd
和.mvn
提交到版本库中,可以使所有开发人员使用统一的Maven版本。:
xxxxxxxxxx
161my-project
2├── .mvn
3│ └── wrapper
4│ ├── MavenWrapperDownloader.java
5│ ├── maven-wrapper.jar
6│ └── maven-wrapper.properties
7├── mvnw
8├── mvnw.cmd
9├── pom.xml
10└── src
11 ├── main
12 │ ├── java
13 │ └── resources
14 └── test
15 ├── java
16 └── resources
只需要把mvn
命令改成mvnw
就可以使用跟项目关联的Maven
1,Socket是一个抽象概念,一个应用程序通过一个Socket来建立一个远程连接,而Socket内部通过TCP/IP协议把数据传输到网络。Socket、TCP和部分IP的功能都是由操作系统提供的,不同的编程语言只是提供了对操作系统调用的简单的封装。Java提供的几个Socket相关的类就封装了操作系统提供的接口。每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。
xxxxxxxxxx
91┌───────────┐
2│Application │
3├───────────┤
4│ Socket │
5├───────────┤
6│ TCP │
7├───────────┤ ┌──────┐
8│ IP │<───>│Router │<────>
9└───────────┘ └──────┘
2,使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收数据。对服务器端来说,它的Socket是指定的IP地址和指定的端口号;对客户端来说,它的Socket是它所在计算机的IP地址和一个由操作系统分配的随机端口号。
3,使用Java进行TCP编程时,需要使用Socket模型:
ServerSocket
监听指定端口;Socket(InetAddress, port)
连接服务器;accept()
接收连接并返回Socket
;如果没有客户端连接进来,accept()
方法会阻塞并一直等待。如果有多个客户端同时连接进来,ServerSocket
会把连接扔到队列里,然后一个一个处理Socket
打开InputStream
/OutputStream
读写数据;当Socket连接创建成功后,无论是服务器端,还是客户端,我们都使用Socket
实例进行网络通信。因为TCP是一种基于流的协议flush()
用于强制输出缓冲区到网络。以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。如果缓冲区的数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()
强制把缓冲区数据发送出去。xxxxxxxxxx
301public class Server {
2 public static void main(String[] args) throws IOException {
3 ServerSocket ss = new ServerSocket(6666);
4 ThreadPoolExecutor threadPoolExecutor= new ThreadPoolExecutor(4,32,30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(16), Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy());
5 for (;;) {
6 Socket sock = ss.accept();
7 threadPoolExecutor.execute(new Handler(sock));
8 }
9 }
10}
11
12class Handler implements Runnable {
13 Socket sock;
14 public Handler(Socket sock) {
15 this.sock = sock;
16 }
17
18 public void run() {
19 try (InputStream input = this.sock.getInputStream()) {
20 try (OutputStream output = this.sock.getOutputStream()) {
21 handle(input, output);
22 }
23 } catch (Exception e) {
24 try {
25 this.sock.close();
26 } catch (IOException ignored) {
27 }
28 }
29 }
30}
xxxxxxxxxx
111public class Client {
2 public static void main(String[] args) throws IOException {
3 Socket sock = new Socket("localhost", 6666);
4 try (InputStream input = sock.getInputStream()) {
5 try (OutputStream output = sock.getOutputStream()) {
6 handle(input, output);
7 }
8 }
9 sock.close();
10 }
11}
1,UDP没有创建连接,数据包也是一次收发一个,所以没有流的概念。
2,UDP端口和TCP端口虽然都使用0~65535,但他们是两套独立的端口,即一个应用程序用TCP占用了端口1234,不影响另一个应用程序用UDP占用端口1234。
3,服务器端
xxxxxxxxxx
151DatagramSocket ds = new DatagramSocket(6666); // 监听指定端口
2for (;;) { // 无限循环
3 // 数据缓冲区:
4 byte[] buffer = new byte[1024];
5 DatagramPacket packet = new DatagramPacket(buffer, buffer.length); // udp没有IO流接口,数据被直接写入byte[]缓冲区。
6 // packet.getSocketAddress() // 获得客户端IP和端口
7 ds.receive(packet); // 收取一个UDP数据包,没收到数据时将会阻塞,
8 // 收取到的数据存储在buffer中,由packet.getOffset(), packet.getLength()指定起始位置和长度
9 // 将其按UTF-8编码转换为String:
10 String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
11 // 发送数据:当服务器收到一个DatagramPacket后,通常必须立刻回复一个或多个UDP包,因为客户端地址在DatagramPacket中,每次循环处理一个客户,所以下一次收到的DatagramPacket可能是不同的客户端,如果不回复,将于客户端失去联系,客户端就收不到任何UDP包。
12 byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);
13 packet.setData(data);
14 ds.send(packet);
15}
客户端
xxxxxxxxxx
131DatagramSocket ds = new DatagramSocket(); // 客户端创建DatagramSocket实例时并不需要指定端口,而是由操作系统自动指定一个当前未使用的端口。
2ds.setSoTimeout(1000); // 后续接收UDP包时,等待时间最多不会超过1秒,否则在没有收到UDP包时,客户端会无限等待下去。
3ds.connect(InetAddress.getByName("localhost"), 6666); // 连接指定服务器和端口,connect()方法不是真连接,它是为了在客户端的DatagramSocket实例中保存服务器端的IP和端口号,确保这个DatagramSocket实例只能往指定的地址和端口发送UDP包,不能往其他地址和端口发送。这么做不是UDP的限制,而是Java内置了安全检查。如果客户端希望向两个不同的服务器发送UDP包,那么它必须创建两个DatagramSocket实例。
4// 发送:
5byte[] data = "Hello".getBytes();
6DatagramPacket packet = new DatagramPacket(data, data.length);
7ds.send(packet); // 通常来说,客户端必须先发UDP包,因为客户端不发UDP包,服务器端就根本不知道客户端的地址和端口号。
8// 接收:
9byte[] buffer = new byte[1024];
10packet = new DatagramPacket(buffer, buffer.length);
11ds.receive(packet); // 收不到数据时阻塞,等待Timeout毫秒后跳出阻塞。
12String resp = new String(packet.getData(), packet.getOffset(), packet.getLength());
13ds.disconnect(); // disconnect()也不是真正地断开连接,它只是清除了客户端DatagramSocket实例记录的远程服务器地址和端口号,这样,DatagramSocket实例就可以连接另一个服务器端。
1,HTTP请求的格式是固定的,它由HTTP Header和HTTP Body两部分构成。第一行总是请求方法 路径 HTTP版本
,例如,GET / HTTP/1.1
表示使用GET
请求,路径是/
,版本是HTTP/1.1
。
如果是GET
请求,那么该HTTP请求只有HTTP Header,没有HTTP Body,GET
请求的参数必须附加在URL上,并以URLEncode方式编码,因为URL的长度限制,GET
请求的参数不能太多。如果是POST
请求,那么该HTTP请求带有Body,以一个空行分隔,body中就是请求的参数,`POST
请求的参数就没有长度限制,因为POST
请求的参数必须放到Body中。
后续的每一行都是固定的Header: Value
格式,我们称为HTTP Header,服务器依靠某些特定的Header来识别客户端请求,例如:
*/*
表示任意格式,text/*
表示任意文本,image/png
表示PNG格式的图片;2,HTTP响应也是由Header和Body两部分组成,响应的第一行总是HTTP版本 响应代码 响应说明
,例如,HTTP/1.1 200 OK
表示版本是HTTP/1.1
,响应代码是200
。客户端只依赖响应代码判断HTTP响应是否成功。HTTP有固定的响应代码:
当浏览器收到第一个HTTP响应后,它解析HTML后,根据解析的内容又会发送一系列HTTP请求,请求需要的资源,例如,GET /logo.jpg HTTP/1.1
请求一个图片,服务器响应图片请求后,会直接把二进制内容的图片发送给浏览器。
服务器总是被动地接收客户端的一个HTTP请求,然后响应它。客户端则根据需要发送若干个HTTP请求。
3,客户端的HTTP编程。
Get请求
xxxxxxxxxx
301import java.net.URI;
2import java.net.http.*;
3import java.net.http.HttpClient.Version;
4import java.time.Duration;
5import java.util.*;
6
7public class Main {
8 // 全局HttpClient:
9 static HttpClient httpClient = HttpClient.newBuilder().build(); // 创建一个全局HttpClient实例,因为HttpClient内部使用线程池优化多个HTTP连接,可以复用,使用链式调用的API,能大大简化HTTP的处理
10
11 public static void main(String[] args) throws Exception {
12 String url = "https://www.sina.com.cn/";
13 HttpRequest request = HttpRequest.newBuilder(new URI(url))
14 // 设置Header:
15 .header("User-Agent", "Java HttpClient").header("Accept", "*/*")
16 // 设置超时:
17 .timeout(Duration.ofSeconds(5))
18 // 设置版本:
19 .version(Version.HTTP_2).build();
20 // 通过内置的BodyHandlers来更方便地处理数据。
21 // 如果要获取图片这样的二进制内容,只需要把HttpResponse.BodyHandlers.ofString()换成HttpResponse.BodyHandlers.ofByteArray(),就可以获得一个HttpResponse<byte[]>对象。如果响应的内容很大,不希望一次性全部加载到内存,可以使用HttpResponse.BodyHandlers.ofInputStream()获取一个InputStream流。
22 HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
23 // HTTP允许重复的Header,因此一个Header可对应多个Value:
24 Map<String, List<String>> headers = response.headers().map();
25 for (String header : headers.keySet()) {
26 System.out.println(header + ": " + headers.get(header).get(0));
27 }
28 System.out.println(response.body().substring(0, 1024) + "...");
29 }
30}
Post请求:要准备好发送的Body数据并正确设置`Content-Type
xxxxxxxxxx
141String url = "http://www.example.com/login";
2String body = "username=bob&password=123456";
3HttpRequest request = HttpRequest.newBuilder(new URI(url))
4 // 设置Header:
5 .header("Accept", "*/*")
6 .header("Content-Type", "application/x-www-form-urlencoded")
7 // 设置超时:
8 .timeout(Duration.ofSeconds(5))
9 // 设置版本:
10 .version(Version.HTTP_2)
11 // 使用POST并设置Body,通过内置的BodyPublishers来更方便地处理数据
12 .POST(BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();
13HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
14String s = response.body();
1,RMI:一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法。提供服务的一方我们称之为服务器,而实现远程调用的一方我们称之为客户端。
2,服务器和客户端必须共享同一个接口。Java的RMI规定此接口必须派生自java.rmi.Remote
,并在每个方法声明抛出RemoteException
xxxxxxxxxx
31public interface WorldClock extends Remote {
2 LocalDateTime getLocalDateTime(String zoneId) throws RemoteException;
3}
xxxxxxxxxx
71// 客户端请求的调用方法getLocalDateTime()最终会通过这个实现类返回结果
2public class WorldClockService implements WorldClock {
3
4 public LocalDateTime getLocalDateTime(String zoneId) throws RemoteException {
5 return LocalDateTime.now(ZoneId.of(zoneId)).withNano(0);
6 }
7}
通过Java RMI提供的一系列底层支持接口,把上面编写的服务以RMI的形式暴露在网络上,客户端才能调用:
xxxxxxxxxx
121public class Server {
2 public static void main(String[] args) throws RemoteException {
3 System.out.println("create World clock remote service...");
4 // 实例化一个WorldClock:
5 WorldClock worldClock = new WorldClockService();
6 // 将此服务转换为远程服务接口:
7 WorldClock skeleton = (WorldClock) UnicastRemoteObject.exportObject(worldClock, 0);
8 // RMI的默认端口是1099,将RMI服务注册到1099端口:
9 Registry registry = LocateRegistry.createRegistry(1099);
10 // 注册此服务,指定服务名为"WorldClock":
11 registry.rebind("WorldClock", skeleton);
12 }
xxxxxxxxxx
121public class Client {
2 public static void main(String[] args) throws RemoteException, NotBoundException {
3 // 连接到服务器localhost,端口1099:
4 Registry registry = LocateRegistry.getRegistry("localhost", 1099);
5 // 查找名称为"WorldClock"的服务并强制转型为WorldClock接口:
6 WorldClock worldClock = (WorldClock) registry.lookup("WorldClock");
7 // 正常调用接口方法:
8 LocalDateTime now = worldClock.getLocalDateTime("Asia/Shanghai");
9 // 打印调用结果:
10 System.out.println(now);
11 }
12}
3,先运行服务器,再运行客户端。RMI通过自动生成stub和skeleton实现网络调用,客户端只需要查找服务并获得接口实例,服务器端只需要编写实现类并注册为服务;从运行结果可知,因为客户端只有接口,并没有实现类,因此,客户端获得的接口方法返回值实际上是通过网络从服务器端获取的。对客户端来说,客户端持有的WorldClock
接口实际上对应了一个“实现类”,它是由Registry
内部动态生成的,并负责把方法调用通过网络传递到服务器端。而服务器端接收网络调用的服务并不是我们自己编写的WorldClockService
,而是Registry
自动生成的代码。我们把客户端的“实现类”称为stub
,而服务器端的网络服务类称为skeleton
,它会真正调用服务器端的WorldClockService
,获取结果,然后把结果通过网络传递给客户端。整个过程由RMI底层负责实现序列化和反序列化,Java的RMI严重依赖序列化和反序列化,而这种情况下可能会造成严重的安全漏洞,因为Java的序列化和反序列化不但涉及到数据,还涉及到二进制的字节码,即使使用白名单机制也很难保证100%排除恶意构造的字节码。因此,使用RMI时,双方必须是内网互相信任的机器,不要把1099端口暴露在公网上作为对外服务。Java的RMI调用机制决定了双方必须是Java程序,其他语言很难调用Java的RMI。如果要使用不同语言进行RPC调用,可以选择更通用的协议,例如gRPC。
1,XML有几个特点:一是纯文本,默认使用UTF-8编码,二是可嵌套,适合表示结构化数据。
2,XML有固定的结构,首行必定是<?xml version="1.0"?>
,可以加上可选的编码。紧接着,如果以类似<!DOCTYPE note SYSTEM "book.dtd">
声明的是文档定义类型(DTD:Document Type Definition),DTD是可选的。接下来是XML的文档内容,一个XML文档有且仅有一个根元素,根元素可以包含任意个子元素,元素可以包含属性,例如,<isbn lang="CN">1234567</isbn>
包含一个属性lang="CN"
,且元素必须正确嵌套。如果是空元素,可以用<tag/>
表示。
由于使用了<
、>
以及引号等标识符,如果内容出现了特殊符号,需要使用&???;
表示转义。例如,Java<tm>
必须写成:
xxxxxxxxxx
11<name>Java<tm></name>
常见的特殊字符如下:
字符 | 表示 |
---|---|
< | < |
> | > |
& | & |
" | " |
' | ' |
格式正确的XML(Well Formed)是指XML的格式是正确的,可以被解析器正常读取。而合法的XML是指,不但XML格式正确,而且它的数据结构可以被DTD或者XSD验证。
3,XML是一种树形结构的文档,它有两种标准的解析API:
4,DOM,顶层的是document代表XML文档,它是真正的“根”
XML的内容:
xxxxxxxxxx
41InputStream input = Main.class.getResourceAsStream("/book.xml");
2DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
3DocumentBuilder db = dbf.newDocumentBuilder();
4Document doc = db.parse(input); // 解析一个XML,它可以接收InputStream,File或者URL,如果解析无误,我们将获得一个Document对象,这个对象代表了整个XML文档的树形结构,需要遍历以便读取指定元素的值:
5,SAX解析会触发一系列事件:
<book>
;</book>
;xxxxxxxxxx
41InputStream input = Main.class.getResourceAsStream("/book.xml");
2SAXParserFactory spf = SAXParserFactory.newInstance();
3SAXParser saxParser = spf.newSAXParser();
4saxParser.parse(input, new MyHandler());
关键代码SAXParser.parse()
除了需要传入一个InputStream
外,还需要传入一个回调对象,这个对象要继承自DefaultHandler
由于SAX没有文件结构,如果要读取<name>
节点的文本,我们就必须在解析过程中根据startElement()
和endElement()
定位当前正在读取的节点,可以使用栈结构保存,每遇到一个startElement()
入栈,每遇到一个endElement()
出栈,这样,读到characters()
时我们才知道当前读取的文本是哪个节点的
6,XML文件结构可以对应到一个定义好的JavaBean,Jackson可以实现XML到JavaBean的转换。
Maven的依赖:
xxxxxxxxxx
111
2<book id="1">
3 <name>Java核心技术</name>
4 <author>Cay S. Horstmann</author>
5 <isbn lang="CN">1234567</isbn>
6 <tags>
7 <tag>Java</tag>
8 <tag>Network</tag>
9 </tags>
10 <pubDate/>
11</book>
xxxxxxxxxx
91public class Book {
2 public long id;
3 public String name;
4 public String author;
5 public String isbn;
6 public List<String> tags;
7 public String pubDate;
8}
9
xxxxxxxxxx
41InputStream input = Main.class.getResourceAsStream("/book.xml");
2JacksonXmlModule module = new JacksonXmlModule();
3XmlMapper mapper = new XmlMapper(module);
4Book book = mapper.readValue(input, Book.class);
7,JSON是JavaScript Object Notation的缩写,它去除了所有JavaScript执行代码,只保留JavaScript的对象格式
xxxxxxxxxx
101{
2 "id": 1,
3 "name": "Java核心技术",
4 "author": {
5 "firstName": "Abc",
6 "lastName": "Xyz"
7 },
8 "isbn": "1234567",
9 "tags": ["Java", "Network"]
10}
几个显著的优点:
\
转义,格式简单;键值对:{"key": value}
,数组:[1, 2, 3]
,字符串:"abc"
,数值(整数和浮点数):12.34
,布尔值:true
或false
,空值:null
JSON也可以转换为Java Bean,依赖:com.fasterxml.jackson.core:jackson-databind:2.10.0
xxxxxxxxxx
81InputStream input = Main.class.getResourceAsStream("/book.json");
2ObjectMapper mapper = new ObjectMapper();
3// 反序列化时忽略不存在的JavaBean属性,使得解析时如果JavaBean不存在该属性时解析不会报错。
4mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
5// f反序列化:Json->JavaBean
6Book book = mapper.readValue(input, Book.class);
7// 序列化:JavaBean->Json
8String json = mapper.writeValueAsString(book);
JSON还支持自定义序列化与反序列化,例如isbn类型不匹配,需要自定义序列化与反序列化器,
xxxxxxxxxx
121// JSON 文件
2{
3 "name": "Java核心技术",
4 "isbn": "978-7-111-54742-6"
5}
6// JavaBean
7public class Book {
8 public String name;
9 // 表示反序列化isbn时使用自定义的IsbnDeserializer:
10 using = IsbnDeserializer.class) (
11 public BigInteger isbn;
12}
xxxxxxxxxx
141public class IsbnDeserializer extends JsonDeserializer<BigInteger> {
2public BigInteger deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
3// 读取原始的JSON字符串内容:
4String s = p.getValueAsString();
5if (s != null) {
6try {
7return new BigInteger(s.replace("-", ""));
8} catch (NumberFormatException e) {
9throw new JsonParseException(p, s, e);
10}
11}
12return null;
13}
14}
1,使用Java程序访问数据库时,Java代码通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。注意到JDBC接口是Java标准库自带的,所以可以直接编译。而具体的JDBC驱动是由数据库厂商提供的,通过引入该厂商提供的JDBC驱动,就可以通过JDBC接口来访问,这样保证了Java程序编写的是一套数据库访问代码,却可以访问各种不同的数据库。Java程序编译期仅依赖java.sql包,不依赖具体数据库的jar包,可随时替换底层数据库,访问数据库的Java代码基本不变。
2,
xxxxxxxxxx
61<dependency>
2 <groupId>mysql</groupId>
3 <artifactId>mysql-connector-java</artifactId>
4 <version>8.0.24</version>
5 <scope>runtime</scope> //编译Java程序并不需要MySQL的这个jar包,只有在运行期才需要使用。如果把runtime改成compile,虽然也能正常编译,但是在IDE里写程序的时候,会多出来一大堆类似com.mysql.jdbc.Connection这样的类,非常容易与Java标准库的JDBC接口混淆,所以坚决不要设置为compile。
6</dependency>
3,类型转换
SQL数据类型 | Java数据类型 |
---|---|
BIT, BOOL | boolean |
INTEGER | int |
BIGINT | long |
REAL | float |
FLOAT, DOUBLE | double |
CHAR, VARCHAR | String |
DECIMAL | BigDecimal |
DATE | java.sql.Date, LocalDate |
TIME | java.sql.Time, LocalTime |
6,插入
1,第一步,通过Connection
提供的createStatement()
方法创建一个Statement
对象,用于执行一个查询,Statment
是需要关闭的资源;
第二步,执行Statement
对象提供的executeQuery("SELECT * FROM students")
并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet
来引用这个结果集,ResultSet
是需要关闭的资源,无论是什么查询语句JDBC查询的返回值总是ResultSet
,即使使用聚合查询也不例外。
第三步,反复调用ResultSet
的next()
方法并读取每一行结果。ResultSet
获取列时,索引从1
开始而不是0
;必须根据SELECT
的列的对应位置来调用getLong(1)
,getString(2)
这些方法,否则对应位置的数据类型不对,将报错。
xxxxxxxxxx
181// JDBC连接的URL, 不同数据库有不同的格式:
2static final String JDBC_URL = "jdbc:mysql://localhost:3306/test";
3static final String JDBC_USER = "root";
4static final String JDBC_PASSWORD = "password";
5// 获取连接:
6try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
7 try (Statement stmt = conn.createStatement()) {
8 try (ResultSet rs = stmt.executeQuery("SELECT id, grade, name, gender FROM students WHERE gender=1")) {
9 while (rs.next()) {
10
11 long id = rs.getLong(1); // 注意:索引从1开始,数据类型要和Selet中国类型顺序预要对应
12 long grade = rs.getLong(2);
13 String name = rs.getString(3);
14 int gender = rs.getInt(4);
15 }
16 }
17 }
18}
使用Statement
拼字符串非常容易引发SQL注入的问题,这是因为SQL参数往往是从方法参数传入的。
xxxxxxxxxx
21// 参数:name = "bob' OR pass=", pass = " OR pass='":,将导致直接跳过认证
2stmt.executeQuery("SELECT * FROM user WHERE login='" + name + "' AND pass='" + pass + "'");
使用PreparedStatement
可以完全避免SQL注入的问题,因为PreparedStatement
始终使用?
作为占位符,并且把数据连同SQL本身传给数据库,这样可以保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。使用Java对数据库进行操作时,必须使用PreparedStatement,严禁任何通过参数拼字符串的代码!
xxxxxxxxxx
141try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
2 try (PreparedStatement ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?")) {
3 ps.setObject(1, "M"); // 必须首先调用setObject()设置每个占位符?的值,注意:索引从1开始
4 ps.setObject(2, 3);
5 try (ResultSet rs = ps.executeQuery()) {
6 while (rs.next()) {
7 long id = rs.getLong("id");// 使用String类型的列名比索引要易读,而且不易出错,也可以是使用下标读取。
8 long grade = rs.getLong("grade");
9 String name = rs.getString("name");
10 String gender = rs.getString("gender");
11 }
12 }
13 }
14}
1,插入
xxxxxxxxxx
221try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO students (id, grade, name, gender) VALUES (?,?,?,?)")) {
2 ps.setObject(1, 999); // 注意:索引从1开始
3 ps.setObject(2, 1); // grade
4 ps.setObject(3, "Bob"); // name
5 ps.setObject(4, "M"); // gender
6 int n = ps.executeUpdate(); //执行更新,返回受影响的记录数量
7}
8// 获取自增主键,指定一个RETURN_GENERATED_KEYS标志位,表示JDBC驱动必须返回插入的自增主键
9try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO students (grade, name, gender) VALUES (?,?,?)",
10 Statement.RETURN_GENERATED_KEYS)) {
11 ps.setObject(1, 1); // grade
12 ps.setObject(2, "Bob"); // name
13 ps.setObject(3, "M"); // gender
14 int n = ps.executeUpdate();
15 // rs对象包含了数据库自动生成的主键的值,读取该对象的每一行来获取每次插入自增主键的值。如果一次插入多条记录,那么这个ResultSet对象就会有多行返回值。如果插入时有多列自增,那么ResultSet对象的每一行都会对应多个自增值(自增列不一定必须是主键)。
16 try (ResultSet rs = ps.getGeneratedKeys()) {
17 if (rs.next()) {
18 long id = rs.getLong(1); // 注意:索引从1开始
19 }
20 }
21}
22
2,删除,更新
xxxxxxxxxx
71// 除了SQL语句不同外其余完全相同
2try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
3 try (PreparedStatement ps = conn.prepareStatement("XXX")) {
4 ps.setObject(1, 999); // 注意:索引从1开始
5 int n = ps.executeUpdate(); // 影响的行数
6 }
7}
1,数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized
同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,ACID准则,Atomicity:原子性,事务的全部修改全部执行完成,或者全部修改失败。,Consistency:一致性,从一个合理的状态转移到另一个合理的状态,Isolation:隔离性,事务感知不到其它事务的存在,Durability:持久性,修改永久反映在DB中。
2,SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:
xxxxxxxxxx
21// 设定隔离级别为READ COMMITTED,MySQL的默认隔离级别是REPEATABLE READ。
2conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
Isolation Level | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
Read Uncommitted | Yes | Yes | Yes |
Read Committed | - | Yes | Yes |
Repeatable Read | - | - | Yes |
Serializable | - | - | - |
Read Uncommitted:可以读取其它事务已经修改但未提交的数据。不存在视图,直接到数据库中读取。事务B修改数据但未提交,事务A读取到B修改的数据,如果B事务回滚或者继续修改,那么之前事务A读到的数据就是脏数据是不存在的,这就是脏读(Dirty Read)。
Read Committed:可以读取其它事务已经提交的数据,每次执行语句前都要重新构建视图。不可重复读是指,在一个事务A内,第一次读一数据,在这个事务A还没有结束时,如果另一个事务B恰好修改并提交了这个数据,第二次A读取的是新的数据,与第一次不一致。通过对读取的行数据加锁解决问题。
Repeatable Read:事务一开始就构建要用到的数据的视图,即使其他事务更改了数据,也不会对视图产生影响,事务类读取的数据始终一致。幻读是指,在一个事务A中,第一次查询(select)某条记录发现没有,之后如果其它事务B添加了该数据并提交,此时A第二次查询(select)某条记录发现还是没有,但当A试图更新这条不存在的记录时竟然能成功,并且A第三次读取同一条记录,它就神奇地出现了。可以通过对表加锁解决问题。
Serializable:所有事务按照次序依次串行执行,因此,脏读、不可重复读、幻读都不会出现。
3,事务的思想
xxxxxxxxxx
161Connection conn = openConnection();
2try {
3 // 关闭自动提交,提交事务的代码在执行完指定的若干条SQL语句后,调用conn.commit()
4 conn.setAutoCommit(false);
5 // 执行多条SQL语句,使用PreparedStatement执行。
6 insert(); update(); delete();
7 // 提交事务:
8 conn.commit();
9 // 如果事务提交失败,会抛出SQL异常,回滚事务,只可以自定义条件出发回滚。
10} catch (SQLException e) {
11 conn.rollback();
12} finally {
13 // 把Connection对象的状态恢复到初始值
14 conn.setAutoCommit(true);
15 conn.close();
16}
默认情况下,我们获取到Connection
连接后,总是处于“自动提交”模式,也就是每执行一条SQL都是作为事务自动执行的。
1,SQL数据库对SQL语句相同,但只有参数不同的若干语句可以作为batch执行,把同一个SQL但参数不同的若干次操作合并为一个batch执行,即批量执行,这种操作有特别优化,速度远远快于循环执行每个SQL。
xxxxxxxxxx
151try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)")) {
2 // 对同一个PreparedStatement反复设置参数并调用addBatch():
3 for (Student s : students) {
4 ps.setString(1, s.name);
5 ps.setBoolean(2, s.gender);
6 ps.setInt(3, s.grade);
7 ps.setInt(4, s.score);
8 ps.addBatch(); // 添加到batch,需要对同一个PreparedStatement反复设置参数并调用addBatch(),这样就相当于给一个SQL加上了多组参数,相当于变成了“多行”SQL。
9 }
10 // 执行batch,返回batch中每个SQL执行的结果数量
11 int[] ns = ps.executeBatch();
12 for (int n : ns) {
13 System.out.println(n + " inserted.");
14 }
15}
1,在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。JDBC连接池有一个标准的接口javax.sql.DataSource
,要使用JDBC连接池,必须选择一个JDBC连接池的实现。常用的JDBC连接池有:HikariCP,C3P0等。
xxxxxxxxxx
71<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->
2<dependency>
3 <groupId>com.zaxxer</groupId>
4 <artifactId>HikariCP</artifactId>
5 <version>3.3.1</version>
6</dependency>
7
xxxxxxxxxx
131//创建连接池,注意创建DataSource也是一个非常昂贵的操作,所以通常DataSource实例总是作为一个全局变量存储,并贯穿整个应用程序的生命周期。
2HikariConfig config = new HikariConfig();
3config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
4config.setUsername("root");
5config.setPassword("password");
6config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒
7config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒,一个连接在空闲一段时间后自动关闭等
8config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10
9DataSource ds = new HikariDataSource(config);
10// 在此获取连接
11try (Connection conn = ds.getConnection()) {
12 ...
13}
一开始,连接池内部并没有连接,所以,第一次调用ds.getConnection()
,会迫使连接池内部先创建一个Connection
,再返回给客户端使用。当我们调用conn.close()
方法时(在try(resource){...}
结束处),不是真正“关闭”连接,而是释放到连接池中,以便下次获取连接时能直接返回。因此,连接池内部维护了若干个Connection
实例,如果调用ds.getConnection()
,就选择一个空闲连接,并标记它为“正在使用”然后返回,如果对Connection
调用close()
,那么就把连接再次标记为“空闲”从而等待下次调用。
1,Java不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带this
参数的函数。
2,函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
3,函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。
4,用Lambda表达式替换单方法接口
xxxxxxxxxx
121Arrays.sort(array, new Comparator<String>() {
2 public int compare(String s1, String s2) {
3 return s1.compareTo(s2);
4 }
5});
6// Lambda方式
7// 参数是(s1, s2),参数类型可以省略,因为编译器可以自动推断出类型,返回值的类型也是由编译器自动推断的。
8Arrays.sort(array, (s1, s2) -> {
9 return s1.compareTo(s2);
10 });
11// 简化版
12Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
5,只定义了单抽象方法(default
方法或static
方法不算入接口定义的抽象方法)的接口称之为FunctionalInterface
,用注解@FunctionalInterface
标记
xxxxxxxxxx
41
2public interface Callable<V> {
3 V call() throws Exception;
4}
1,可以向方法传入Lambda表达式和方法引用。
所谓方法引用,是指如果某个方法签名(参数列表和返回值列表)和接口恰好一致,就可以直接传入方法引用。(鸭子类型)
xxxxxxxxxx
91public static void main(String[] args) {
2 String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
3 Arrays.sort(array, Main::cmp);
4 Arrays.sort(array, (s1, s2) -> {return s1.compareTo(s2);});
5}
6// 因为Comparator<String>接口定义的方法是int compare(String, String)和cmp拥有相同的方法签名(参数列表和返回值列表),可以直接把方法名作为Lambda表达式传入
7static int cmp(String s1, String s2) {
8 return s1.compareTo(s2);
9}
2,某些方法要求传入一个FunctionalInterface的实现类,可以向他传递该FunctionalInterface中定义方法的方法引用。FunctionalInterface
不强制继承关系,不需要方法名称相同,只要求方法参数(类型和数量)与方法返回类型相同,即认为方法签名相同。
可以传入静态方法,实例方法,构造方法,
xxxxxxxxxx
71// 期望方法:int Comparator<String>.compare(String, String)
2// 传入静态方法,cmp完全满足该要求。
3Arrays.sort(array, Main::cmp);
4// 传入实例方法,string 中方法签名:int compareTo(String o),但实例方法的第一个隐含参数总是传入this,即是int compareTo(this, String o);this属于String类型,所以满足要求。
5Arrays.sort(array, String::compareTo);
6// 构造方法,ClassName::new,构造方法虽然没有return语句,但它会隐式地返回this实例,map接受的方法 R apply(T t);与Persion构造方法:public Person(String name) {this.name = name;},此处T为String,R为Persion,apply为构造方法,隐式地返回Persion实例,与要求相匹配。
7List<Person> persons = List.of("Bob", "Alice", "Tim").stream().map(Person::new).collect(Collectors.toList());
1,和文件流区别
java.io | java.util.stream | |
---|---|---|
存储 | 顺序读写的byte 或char | 顺序输出的任意Java对象实例 |
用途 | 序列化至文件或网络 | 内存计算/业务逻辑 |
2,和集合区别
List
存储的每个元素都是已经存储在内存中的某个Java对象,而Stream
输出的元素可能并没有预先存储在内存中,而是实时计算出来的。List
的用途是操作一组已存在的Java对象,而Stream
实现的是惰性计算,真正的计算通常发生在最后结果的获取,也就是惰性计算。一个Stream
转换为另一个Stream
时,实际上只存储了转换规则,并没有任何计算发生。它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。
java.util.List | java.util.stream | |
---|---|---|
元素 | 已分配并存储在内存 | 可能未分配,实时计算 |
用途 | 操作一组已存在的Java对象 | 惰性计算 |
xxxxxxxxxx
61Stream<BigInteger> naturals = createNaturalStream(); // 不计算
2Stream<BigInteger> s2 = naturals.map(BigInteger::multiply); // 不计算
3Stream<BigInteger> s3 = s2.limit(100); // 不计算
4s3.forEach(System.out::println); // 计算
5// Stream API支持函数式编程和链式操作;
6createNaturalStream().map(BigInteger::multiply).limit(100).forEach(System.out::println);
3,创建Stream
Stream.of()静态方法:传入可变参数即创建了一个能输出确定元素的Stream
,
xxxxxxxxxx
11Stream<String> stream = Stream.of("A", "B", "C", "D");
基于一个数组或者Collection
:
xxxxxxxxxx
41// 把数组变成Stream使用Arrays.stream()方法
2Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
3// 对于Collection(List、Set、Queue等),直接调用stream()方法
4Stream<String> stream2 = List.of("X", "Y", "Z").stream();
Java的范型不支持基本类型,如果使用包装类会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream
、LongStream
和DoubleStream
这三种使用基本类型的Stream
,它们的使用方法和范型Stream
没有大的区别,设计这三个Stream
的目的是提高运行效率:
xxxxxxxxxx
41// 将int[]数组变为IntStream:
2IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
3// 将Stream<String>转换为LongStream:
4LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
Supplier:基于Supplier
创建的Stream
会不断调用Supplier.get()
方法来不断产生下一个元素,这种Stream
保存的不是元素,而是算法,它可以用来表示无限序列,Stream
几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。
xxxxxxxxxx
91Stream<String> s = Stream.generate(Supplier<String> sp);
2// sp要实现Supplier<T>接口,同时提供LongSuppliers等对基本数据内心的支持。
3class NatualSupplier implements Supplier<Integer> {
4 int n = 0;
5 public Integer get() {
6 n++;
7 return n;
8 }
9}
Files
类:Files
类的lines()
方法可以把一个文件变成一个Stream
,每个元素代表文件的一行内容
xxxxxxxxxx
31try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
2 ...
3}
正则,plitAsStream()
方法把一个长字符串分割成Stream
序列而不是数组
xxxxxxxxxx
31Pattern p = Pattern.compile("\\s+");
2Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");
3s.forEach(System.out::println);
1,通过吧一个Steam中的每个元素通过某种运算得到结果,把结果加入另一个Stream,实现把一个Stream
转换为另一个Stream
。map()
方法接收的对象是Function
接口对象,它定义了一个apply()
方法,负责把一个T
类型转换成R
类型,可以传入现有方法,或者是Lambda表达式:
xxxxxxxxxx
71<R> Stream<R> map(Function<? super T, ? extends R> mapper); //T只读,R只写
2// Function定义
3
4public interface Function<T, R> {
5 // 将T类型转换为R:
6 R apply(T t);
7}
1,filter()
操作,就是对一个Stream
的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream
。
2,filter()
方法接收的对象是Predicate
接口对象,它定义了一个test()
方法,负责判断元素是否符合条件。传入lambda表达式或者满足满足该方法签名的方法或者该接口的实现类。
xxxxxxxxxx
51
2public interface Predicate<T> {
3 // 判断元素t是否符合条件:
4 boolean test(T t);
5}
1,Stream.reduce()
则是Stream
的一个聚合方法,它可以把一个Stream
的所有元素按照聚合函数聚合成一个结果。
2,reduce()
方法传入的对象是BinaryOperator
接口,它定义了一个apply()
方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果,reduce()
方法将一个Stream
的每个元素依次作用于BinaryOperator
,并将结果合并。reduce()
是聚合方法,聚合方法会立刻对Stream
进行计算。流的特点是每个元素只遍历一次,聚合操作无非就是一个遍历+累加计算的过程,所以使用reduce后流就已经关闭,无法再次遍历流。
xxxxxxxxxx
91
2public interface BinaryOperator<T> {
3 // Bi操作:两个输入,一个输出
4 T apply(T t, T u);
5}
6// initValue为初始值,初始acc=initValue;n为Stream中元素,acc=acc*n;然后再次调用lambda表达式不断处理Stream中元素,最后返回acc
7reduce(initValue, (acc, n) -> acc * n);
8// reduce 也可以操作复杂Java对象,其中原始stream 为Map构成的Stream。
9reduce(new HashMap<String, String>(), (m, kv) -> {m.putAll(kv);return m;})
1,对于Stream
来说,对其进行转换操作(map(),
filter(),
sorted(),
distinct())并不会触发任何计算,转换操作只是保存了转换规则,无论我们对一个Stream
转换多少次,都不会有任何实际计算发生。聚合操作(reduce(),
collect(),
count(),
max(),
min(),
sum(),
average())会立刻促使Stream
输出它的每一个元素,并依次纳入计算,以获得最终结果。所以,对一个Stream
进行聚合操作,会触发一系列连锁反应。聚合方法会立刻对Stream
进行计算。流的特点是每个元素只遍历一次,聚合操作无非就是一个遍历+累加计算的过程,所以使用聚合操作后流就已经关闭,无法再次遍历流。
2,把Stream
变为List
/Array
不是一个转换操作,而是一个聚合操作,它会强制Stream
输出每个元素。
xxxxxxxxxx
61// 调用collect()并传入Collectors.toList()对象,它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。
2collect(Collectors.toList());
3// 把Stream的每个元素收集到Set中
4collect(Collectors.toSet());
5// 把Stream的每个元素收集到数组中,要传入数组的“构造方法
6toArray(String[]::new);
3,输出为Map,方法接受两个方法参数,第一个返回key,第二个返回value。
xxxxxxxxxx
51collect(Collectors.toMap(
2 // 把元素s映射为key:
3 s -> s.substring(0, s.indexOf(':')),
4 // 把元素s映射为value:
5 s -> s.substring(s.indexOf(':') + 1)));
4,分组输出,需要提供两个函数:一个是返回单个元素用于分组的key,第二个是如何处理聚集到一起的多个元素,可以再次对他们分组。
xxxxxxxxxx
21Map<String, List<String>> groups =collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
2Map<Integer, Map<Integer, List<Student>>> groups = studentStream.collect( Collectors.groupingBy(s -> s.gradeId, Collectors.groupingBy(s -> s.classId, Collectors.toList())));
1,
sorted():排序,要求Stream
的每个元素必须实现Comparable
接口。如果要自定义排序,传入指定的Comparator
即可。转换操作,它会返回一个新的Stream
xxxxxxxxxx
21sort() //每个元素必须实现`Comparable`接口
2sorted(String::compareToIgnoreCase) // 传入指定的Comparator
distinct():去重,正确实现equals,或者传入只定义比较器。截取操作也是一个转换操作,将返回新的Stream
concat():合并两个流,Stream.concat(s1, s2);
flatMap():把Stream
的每个元素映射为Stream
,然后合并成一个新的Stream
xxxxxxxxxx
51Stream<Integer> i = Stream.of(
2 Arrays.asList(1, 2, 3),
3 Arrays.asList(4, 5, 6),
4 Arrays.asList(7, 8, 9)
5 ).flatMap(list -> list.stream());
parallel():一个普通Stream
转换为可以并行处理的Stream
,通常情况下,对Stream
的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream
的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。经过parallel()
转换后的Stream
只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。注意对线程不安全(如hashMap)的集合的操作的并发安全性问题。
forEach():相当于内部循环调用,它可以循环处理Stream
的每个元素,可传入符合Consumer接口的void accept(T t)
的方法引用,如stream.forEach(System.out::println);
limit(n):截断
count():计数
max(Comparator<? super T> cp):找出最大元素;
min(Comparator<? super T> cp):找出最小元素。
sum():对所有元素求和;
average():对所有元素求平均数
boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。
summaryStatistics()一次性终端操作获得{count
, sum
, min
,average
, max
}等统计数据。
2,分类
转换操作:map()
,filter()
,sorted()
,distinct()
;
合并操作:concat()
,flatMap()
;
并行处理:parallel()
;
聚合操作:reduce()
,collect()
,count()
,max()
,min()
,sum()
,average()
;
其他操作:allMatch()
, anyMatch()
, forEach()
。
可被传入当作满足函数接口要求的参数可分为四类
接口的实现类
接口的匿名实现类
静态方法
实例方法,注意this参数
Lambda表达式