Java高级编程
第一章 多线程
基本概念:程序、进程、线程
程序(program):程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):进程是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期如:
- 运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread):,进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
单核CPU和多核CPU的理解
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程 的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费 才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以 把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时 间单元特别短,因此感觉不出来。
- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
- 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
个人理解为:单核就是一个cpu在一段时间可以处理多个线程,在一个单位的时间只能处理一个事情,它把多个线程按照某种顺序来执行,所以在一段时间内就是处理了多个线程,所以说是假的多线程。
第三点可以理解为:一个main主线程里面可以有其他的线程,例如main方法里面调用别的方法,当内部这个方法执行完毕后就会有gc() 来回收,当然不可以让main主线程来做这个事情,应为main主线程还有其他的线程要做。在main内部的方法执行的时候可能出现异常,这个异常当然不可让main以及main内部产生这个异常的方法来处理,这时就有一个异常处理的线程来工作,所以一个java.exe 至少会有3个线程工作。
并行与并发
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事
- 使用多线程的优点
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
线程的创建和使用
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。
Thread类的特性:
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常 把run()方法的主体称为线程体
- 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
Thread类的构造器
- Thread():创建新的Thread对象
- Thread(String threadname):创建线程并指定线程实例名
- Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
- Thread(Runnable target, String name):创建新的Thread对象
- API中创建线程的两种方式
JDK1.5之前创建新执行线程有两种方法:①继承Thread类的方式;②实现Runnable接口的方式;
- 定义子类继承Thread类。
- 子类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39//1. Declare a class to be a subclass of Thread
class EvenNumber extends Thread {
int number = 0;
public EvenNumber(int number) {
this.number = number;
}
// 2. Override the run method of class Thread.
public void run() {
for (int i = 0; i < number; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i + " is a even number");
}
}
}
}
public class ThreadTest01 {
public static void main(String[] args) {
// 3.Build an instance of the subclass
EvenNumber n = new EvenNumber(10);
// 4. This instance can be allocated and started
n.start();
// This code will be executed in the main method
for (int i = 0; i < n.number; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i + " is a even number");
}
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13D:\PATH-EN\java-path\bin\java.exe ...
main:0 is a even number
Thread-0:0 is a even number
Thread-0:2 is a even number
Thread-0:4 is a even number
Thread-0:6 is a even number
Thread-0:8 is a even number
main:2 is a even number
main:4 is a even number
main:6 is a even number
main:8 is a even number
Process finished with exit code 0
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 1. Create a thread is to declare a class that implements the Runnable interface.
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
// 2.This class then implements the run method.
public void run() {
// Compute primes larger than minPrime
}
}
public class ThreadTest02 {
public static void main(String[] args) {
// 3. Create an instance of PrimeRun class
PrimeRun p = new PrimeRun(143);
// When you create a thread put the instance as arguments and start it
new Thread(p).start();
new Thread(p).start();
}
}
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
- Thread类的有关方法
- start():启动当前线程;调用当前线程的run()
- run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
- currentThread():静态方法,返回执行当前代码的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():释放当前cpu的执行权。(通俗讲就是:停止后重新分配)
- join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。(通俗讲就是:插队)
- stop():已过时。当执行此方法时,强制结束当前线程。
- sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。(通俗讲就是:暂停多少毫秒)
- isAlive():判断当前线程是否存活
- 线程的优先级
1
2
3MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5
- 涉及的方法
getPriority() :返回线程优先值
setPriority(int newPriority) :改变线程的优先级 - 说明
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
- Java中的线程分为两类
- 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
- 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
- Java垃圾回收就是一个典型的守护线程。
- 若JVM中都是守护线程,当前JVM将退出。
线程的生命周期
- JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
点击下方图片查看Thread.State源码,Ctrl+F 搜索”public enum State”查看状态
注意:只有处于NEW状态的线程可以调用start()方法,处于其他状态的线程都不可以调用start()方法,否则将触发IllegalThreadStateException异常
线程的同步
通过Java可以创建多个线程,用户在处理多线程问题时,必须注意这样一个问题:当两个或多个线程同时访问同一个变量,并且一个线程需要修改这个变量时,应对这样的问题做出处理,否则可能发生混乱。例如,一个工资管理负责人正在修改雇员的工资表,而一些雇员正在领取工资,如果允许这样做,必然会出现混乱。因此,工资管理负责人正在修改工资表时(包括他喝杯茶休息一会儿(),不允许任何雇员领取工资,也就是说,这些雇员必须等待。
- Java对于多线程的安全问题提供了专业的解决方式:同步机制
- 同步代码块
1
2
3synchronized(同步监视器){
// 需要被同步的代码
} - synchronized还可以放在方法声明中,表示整个方法为同步方法。
1
2
3public synchronized void show (String name){
........
}共享数据:多个线程共同操作的变量。
同步监视器:俗称”锁”。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
模拟火车站售票程序,开启三个窗口售票。
1 | class Window extends Thread{ |
1 | class Window1 implements Runnable{ |
问题:卖票过程中,出现了重票、错票 —> 出现了线程的安全问题
问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他
线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
1 | public class Windows01Test { |
1 | public class WindowsTest02 { |
1 | public class Windows03Test { |
1 | public class WindowsTest04 { |
由于每次执行的结果都不一样,但是从执行结果来看已经解决了可能发生的重票和错票问题
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
针对于线程的同步个人理解:线程同步解决了线程安全的问题,线程安全问题的产生就是可能多个线程同时处理了线程们共用的属性。解决的思路也就是让线程们公用的属性在某一时间只能被一个线程使用。用一个“锁”来形容就是,当某个线程处理公共的属性时,给这个属性添加“锁”,处理完了在打开,所以只有当某个线程处理完了,下一个线程才可以接手。当然刚处理完的线程和其他线程享有平等的处理属性的权力。
针对于上方售票例子个人理解:我们把线程们公用的操作叫做共享数据,同步监视器可以是任意的对象,但有一个要求就是,多个线程公用一个锁即同一个对象,对于实现的方式来说,我们只做了一次的 TicketWindows tw = new TicketWindows(); 操作,所以synchronized(this),就指的是 tw。但是对于继承的方式来说我们做了三次的TicketWindows02 t1 = new TicketWindows02();
TicketWindows02 t2 = new TicketWindows02();TicketWindows02 t3 = new TicketWindows02();操作,每一次new都是一个新的对象。所以此时的 this 就分别是t1,t2,t3,不是同一个对象,所以不可以用 this ,这是就引入了,Class.class,我们目前没有学过这个东西,只需要知道类也是对象。且Class.class只生成一次。另外对于synchronized大括号包裹的范围不可以多包,也不可以少包裹,少包很好解释就是没有包裹的就不会被同步,这对于我们来说是肯定不可以发生的,多包的话有时候很难发现,例如上方1.使用同步代码块处理上方实现产生的线程不安全问题第29行代码while(true),如果第30行在第29行上面。这就会产生问题。运行会发现,一直只是一个窗口在处理票,不会有其他的窗口参与。这其实也很好解释,如果synchronized同步了while(true),就说明,开始时某一个线程抢得执行权,然后一直都是这个线程内部在死循环,直接这个线程内部把票给处理完了,其他得线程根本进不来,所以就产生了这种的现象。
- 解决单例设计模式懒汉式线程不安全问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 方式一
public class SingletonTest {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
class Singleton {
private Singleton() {
}
private static Singleton instance = null;
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 方式二
public class SingletonTest {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
class Singleton {
private Singleton() {
}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}点击查看运行结果
1
2
3
4D:\PATH-EN\java-path\bin\java.exe ...
true
Process finished with exit code 01
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class SingletonTest {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
class Singleton {
private Singleton() {
}
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}点击查看运行结果
1
2
3
4D:\PATH-EN\java-path\bin\java.exe ...
true
Process finished with exit code 0个人理解:上面的同步代码块实现解决懒汉式方式一效率相较方式二稍差些,可以举例:例如很多人都想去商店买商品,如果此时商品只有一个,但很多人都要买,就需要每个人都去商店询问是否还有商品,如果此时在门口放个牌子说明东西售罄,则后来者就不需要每个人都进店询问了。方式二就是类似,如果new过一次实例则instance一定 != null,故里面的就不需要每次都new都分配内存空间。如果某个线程第一次判断instance==null,就需要进去new,则方式二对于后来的线程则可以不用进去new。
同步机制中的锁
- 同步锁机制:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁 之时,另一个任务就可以锁定并使用它了。
- synchronized的锁是什么?
- 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
- 同步方法的锁:静态方法(类名.class)、非静态方法(this)
- 同步代码块:自己指定,很多时候也是指定为this或类名.class
- 注意:
- 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
- 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方 法共用同一把锁(this),同步代码块(指定需谨慎)
同步的范围
- 如何找问题,即代码是否存在线程安全?(非常重要)
明确哪些代码是多线程运行的代码
明确多个线程是否有共享数据
明确多线程运行代码中是否有多条语句操作共享数据 - 如何解决呢?(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其 他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中 - 切记:
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。
- 释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线 程暂停,并释放锁。
- 不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程
线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决办法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
死锁的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60public class DeadLockTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread() {
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}点击查看运行结果
1
2D:\PATH-EN\java-path\bin\java.exe ...
锁
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以 显式加锁、释放锁。
使用锁来解决线程安全问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50public class LockTest {
public static void main(String[] args) {
Lock lock = new Lock();
Thread thread1 = new Thread(lock);
Thread thread2 = new Thread(lock);
Thread thread3 = new Thread(lock);
thread1.setName("线程一");
thread2.setName("线程二");
thread3.setName("线程三");
thread1.start();
thread2.start();
thread3.start();
}
}
class Lock implements Runnable {
private ReentrantLock lock = new ReentrantLock();
private int num = 200;
public void run() {
while (true) {
try {
lock.lock();
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
num--;
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15D:\PATH-EN\java-path\bin\java.exe ...
线程二:200
线程二:199
线程二:198
线程二:197
.......
.......
.......
线程一:5
线程一:4
线程一:3
线程一:2
线程一:1
Process finished with exit code 0
synchronized与Lock的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
线程间的通信问题
问题引入:使用两个线程打印 1-100。线程1, 线程2 交替打印
1 | public class CommunicationTest { |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
这样理解:刚开始两个线程某个抢得执行权,进来后查看没有wait的线程,所以执行下面代码.当执行到wait()后这个线程等待,释放锁,则另一个线程,也只有另外这个线程获得执行权.这个线程进来后发现有一个线程在wait,就执行notify代码,前一个线程就被释放了.这时前一个线程进入RUNABLE状态等待重新分配.此时已经进入的线程就执行run.到达wait后等待,且释放锁,这时被在RUNABLE的线程捕获进入,依次循环
wait() 与 notify() 和 notifyAll()
- wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
- notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
- notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
注意:这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
wait()方法特点
- 在当前线程中调用方法: 对象名.wait()
- 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify(或notifyAll) 为止。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
- 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。
- notify()/notifyAll()特点
- 在当前线程中调用方法: 对象名.notify()
- 功能:唤醒等待该对象监控权的一个/所有线程。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 综合问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者一:");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者一:");
p1.start();
c1.start();
}
}
class Clerk {
private int productCount = 0;
public synchronized void produceProduct() {
if (productCount < 20) {
productCount++;
System.out.println(Thread.currentThread().getName()
+ "开始生产第:" + productCount + "个产品");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumeProduct() {
if (productCount > 0) {
System.out.println(Thread.currentThread().getName()
+ "开始消费第:" + productCount + "个产品");
productCount--;
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println(Thread.currentThread().getName()
+ "开始生产....");
while (true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println(Thread.currentThread().getName()
+ "开始消费....");
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27D:\PATH-EN\java-path\bin\java.exe ...
生产者一:开始生产....
消费者一:开始消费....
生产者一:开始生产第:1个产品
消费者一:开始消费第:1个产品
生产者一:开始生产第:1个产品
生产者一:开始生产第:2个产品
消费者一:开始消费第:2个产品
生产者一:开始生产第:2个产品
生产者一:开始生产第:3个产品
消费者一:开始消费第:3个产品
生产者一:开始生产第:3个产品
消费者一:开始消费第:3个产品
生产者一:开始生产第:3个产品
.......
.......
生产者一:开始生产第:20个产品
消费者一:开始消费第:20个产品
生产者一:开始生产第:20个产品
消费者一:开始消费第:20个产品
生产者一:开始生产第:20个产品
消费者一:开始消费第:20个产品
生产者一:开始生产第:20个产品
消费者一:开始消费第:20个产品
生产者一:开始生产第:20个产品
Process finished with exit code 130
JDK5.0新增线程创建方式
新增方式一:实现Callable接口
与使用Runnable相比, Callable功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是 否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
创建的步骤:
- 创建一个实现Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 获取Callable中call方法的返回值get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
1 | import java.util.concurrent.Callable; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
新增方式二:使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完 放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交 通工具。
好处:- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- …
线程池相关API
- JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable - void shutdown() :关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
步骤
- 提供指定线程数量的线程池
设置线程池的属性 - 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
- 关闭连接池
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43public class ThreadPoolTest {
public static void main(String[] args) {
// 1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new EvenNum());
service.execute(new OddNumber());
// 3. 关闭连接池
service.shutdown();
// service.submit();
}
}
class EvenNum implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() +
":" + i);
}
}
}
}
class OddNumber implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() +
":" + i);
}
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13D:\PATH-EN\java-path\bin\java.exe ...
pool-1-thread-1:0
pool-1-thread-1:2
pool-1-thread-1:4
pool-1-thread-1:6
pool-1-thread-1:8
pool-1-thread-2:1
pool-1-thread-2:3
pool-1-thread-2:5
pool-1-thread-2:7
pool-1-thread-2:9
Process finished with exit code 0
第二章 Java常用类
字符串相关的类
String的特性
- String类:代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。
- String是一个final类,代表不可变的字符序列。
- 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改。
- String对象的字符内容是存储在一个字符数组value[]中的。
String str1 = “abc”;与String str2 = new String(“abc”);的区别?
- 字符串常量存储在 字符串常量池,目的是共享
- 字符串非常量对象存储在堆中。
例子
1 | import org.testng.annotations.Test; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
test01个人理解:字符串有一个特点就是不可变性,所以根据这个特性可以知道,只要时需要变化的字符串都是在方法区的字符串常量池里面重新生成一个新的目的字符串,例如 s1 = “abc” +123,其实不是改变 abc ,而是生成一个新的字符串”abc123”然后新的字符串的地址指向s1。严格按照字符串的不可变性可类比其他的字符串操作。且需要注意的是,字符串常量池是不会存储相同的字符串的。
test02个人理解:我们可以知道,只要new一次就是一个新的对象。且new出来的对象都是存储在堆里面的。那么
像new String(“Jermyn”)的特点就是,一个new的对象,但是括号里面的Jermyn字符串是存储在字符串常量池里面,类比,如果new了两次,显然是两个对象,但是他们的内容都是指向常量池里面的数据。所以可以知道,如果是new了两个相同的对象,他们显然是不相等的,虽然面的数据都是指向常量池里里面的同一个数据。
test03个人理解:只需要记住如果变量参与计算如:s1 += “abc”,此种都是在堆中生成的,相当于new。调用inten()方法返回的值在常量池
字符串相关的方法
- 字符串相关的类:String常用方法1
- int length():返回字符串的长度: return value.length
- char charAt(int index): 返回某索引处的字符return value[index]
- boolean isEmpty():判断是否是空字符串:return value.length == 0
- String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
- String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
- String trim():返回字符串的副本,忽略前导空白和尾部空白
- boolean equals(Object obj):比较字符串的内容是否相同
- boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
- String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
- int compareTo(String anotherString):比较两个字符串的大小
- String substring(int beginIndex): 返回一个新的字符串, 它是此字符串的从
- beginIndex开始截取到最后的一个子字符串。
- String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字 符串从beginIndex开始截取endIndex(不包含)的一个子字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import org.testng.annotations.Test;
import java.util.Locale;
public class StringMethodTest {
String string01 = " Jermyn . cn ";
String string02 = "";
public void test01() {
// 1. int length():返回字符串的长度return value.length
System.out.println(string01.length());
}
public void test02() {
//2.char charAt(int index): 返回某索引处的字符return value[index]
System.out.println(string01.charAt(1));
System.out.println(string01.charAt(11));
// System.out.println(string01.charAt(13));
}
public void test03() {
//3. boolean isEmpty():判断是否是空字符串:return value.length == 0
System.out.println(string01.isEmpty());
System.out.println(string02.isEmpty());
}
public void test04() {
//4.1 String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
//4.2 String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
System.out.println(string01.toLowerCase(Locale.ROOT));
System.out.println(string01.toUpperCase(Locale.ROOT));
}
public void test05() {
// 5.String trim():返回字符串的副本,忽略前导空白和尾部空白
System.out.println(string01.trim());
}
public void test06() {
//6.boolean equals(Object obj):比较字符串的内容是否相同
System.out.println(string01.equals(string02));
}
public void test07() {
// 7.boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
String s1 = "JERMYN";
String s2 = "Jermyn";
System.out.println(s1.equalsIgnoreCase(s2));
}
public void test08() {
// 8.String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
String s1 = "Jermyn";
String s2 = ".cn";
System.out.println(s1.concat(s2));
}
public void test09() {
// 9.int compareTo(String anotherString):比较两个字符串的大小
// 负数当前对象小,零对象相等,整数当前对象大
String s1 = "Jermyn";
String s2 = "Jermyn_";
String s3 = "Jermy";
System.out.println(s1.compareTo(s2));
System.out.println(s1.compareTo(s3));
}
public void test10() {
// 10. String substring(int beginIndex): 返回一个新的字符串, 它是此字符串的从beginIndex开始截取到最后的一个子字符串。
String s1 = new String("https://www.jermyn.cn");
System.out.println(s1.substring(8));
}
public void test11() {
// 11.String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字 符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
String s1 = new String("https://www.jermyn.cn");
System.out.println(s1.substring(12,18));
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24D:\PATH-EN\java-path\bin\java.exe ...
13
J
n
false
true
jermyn . cn
JERMYN . CN
Jermyn . cn
false
true
Jermyn.cn
-1
1
www.jermyn.cn
jermyn
===============================================
Default Suite
Total tests run: 11, Passes: 11, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
- 字符串相关的类:String常用方法2
- boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
- boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
- boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
- boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列 时,返回 true
- int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
- int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出 现处的索引,从指定的索引开始
- int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
- int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后 一次出现处的索引,从指定的索引开始反向搜索注:indexOf和lastIndexOf方法如果未找到都是返回-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71import org.testng.annotations.Test;
public class StringMethodTest02 {
public void test01() {
// 1.boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
String s1 = new String("https://www.jermyn.cn");
System.out.println(s1.endsWith(".cn"));
System.out.println(s1.endsWith(".com"));
}
public void test02() {
//2.boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
String s1 = new String("https://www.jermyn.cn");
System.out.println(s1.startsWith("https://"));
System.out.println(s1.startsWith("http://"));
}
public void test03() {
// 3.boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
String s1 = new String("https://www.jermyn.cn");
System.out.println(s1.startsWith("www", 8));
}
public void test04() {
// 4.boolean contains(CharSequence s):当且仅当此字符串包含指定的char值序列时,返回true
String s1 = new String("https://www.jermyn.cn");
System.out.println(s1.contains("www."));
System.out.println(s1.contains("Jermyn"));
}
public void test05() {
// 5.1 int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
String s1 = new String("https://www.jermyn.cn");
System.out.println(s1.indexOf("www"));
System.out.println(s1.indexOf("wws"));
// 5.2 int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
System.out.println(s1.indexOf("www",9));
}
public void test06() {
// 6. int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
String s1 = new String("https://www.jermyn.cn");
System.out.println(s1.lastIndexOf(".cn"));
}
public void test07() {
// 7. int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后 一次出现处的索引,从指定的索引开始反向搜索
//注:indexOf和lastIndexOf方法如果未找到都是返回-1
String s1 = new String("https://www.jermyn.cn");
System.out.println(s1.lastIndexOf("www", 12));
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21D:\PATH-EN\java-path\bin\java.exe ...
true
false
true
false
true
true
false
8
-1
-1
18
8
===============================================
Default Suite
Total tests run: 7, Passes: 7, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
- 字符串相关的类:String常用方法3
- String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
- String replace(CharSequence target, CharSequence replacement): 使 用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
- String replaceAll(String regex, String replacement) : 使用给定的replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
- String replaceFirst(String regex, String replacement) : 使用给定的replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
- boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。
- String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
- String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此 字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71import org.testng.annotations.Test;
public class StringMethodTest03 {
public void test01() {
//1. String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用newChar 替换此字符串中出现的所有 oldChar 得到的。
String s1 = new String("https://jermyn.cn");
// 所有的 char 都替换
System.out.println(s1.replace("j", "J"));
// 1.1 String replace(CharSequence target, CharSequence replacement): 使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
// 所有的 char[] 都替换
System.out.println(s1.replace("jermyn", "JERMYN"));
}
public void test02() {
// 2. String replaceAll(String regex, String replacement) :使用给定的replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
String s1 = "12hello34world5java7891mysql456";
// 其实正则的表达大同小异,此处不在详细说明,如果需要的话可以查看博客《shell学习笔记》第14,15章
// https://www.jermyn.cn/posts/d746.html 会有一定启发
String s2 = s1.replaceAll("\\d+", ",").replaceAll("^,|,$", "");
System.out.println(s2);
}
public void test03() {
// 3. boolean matches(String regex):告知此字符串是否匹配给定的正则表达式
String str = "12345";
//判断str字符串中是否全部有数字组成,即有1-n个数字组成 boolean matches = str.matches("\\d+"); System.out.println(matches);
String tel = "0571-4534289";
//判断这是否是一个杭州的固定电话
boolean result = tel.matches("0571-\\d{7,8}");
System.out.println(result);
}
public void test04() {
// 4.String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
String str = "hello|world|java";
String[] strs = str.split("\\|");
for (int i = 0; i < strs.length; i++) {
System.out.println(strs[i]);
}
System.out.println();
}
public void test05() {
// 5. String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此 字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中
String str2 = "hello.world.java";
String[] strs2 = str2.split("\\.");
for (int i = 0; i < strs2.length; i++) {
System.out.println(strs2[i]);
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20D:\PATH-EN\java-path\bin\java.exe ...
https://Jermyn.cn
https://JERMYN.cn
hello,world,java,mysql
true
hello
world
java
hello
world
java
===============================================
Default Suite
Total tests run: 9, Passes: 9, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
字符串相关的类:String与基本数据类型转换
字符串—>基本数据类型、包装类
- Integer包装类的public static int parseInt(String s):可以将由“数字”字 符组成的字符串转换为整型。
- 类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型
基本数据类型、包装类—>字符串
- 调用String类的public String valueOf(int n)可将int型转换为字符串
- 相应的valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(doubled)、valueOf(boolean b)可由参数的相应类型到字符串的转换
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import org.testng.annotations.Test;
public class StringTest {
public void test01() {
// String --> char[]调用String的toCharArray()
String str = "Jermyn.cn";
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length; i++) {
System.out.println(chars[i]);
}
// char[] --> String 调用String的构造器
char[] arr = new char[]{'j', 'e', 'r', 'm', 'y', 'n'};
String s = new String(arr);
System.out.println(s);
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19D:\PATH-EN\java-path\bin\java.exe ...
J
e
r
m
y
n
.
c
n
jermyn
===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
字符串相关的类:String与字节数组转换
字节数组—>字符串
- String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
- String(byte[],int offset,int length) :用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。
字符串—>字节数组
- public byte[] getBytes() :使用平台的默认字符集将此 String 编码为byte 序列,并将结果存储到一个新的 byte 数组中。
- public byte[] getBytes(String charsetName) :使用指定的字符集将 此 String 编码到 byte 序列,并将结果存储到新的 byte 数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28import org.testng.annotations.Test;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
public class StringTest {
public void test01() throws UnsupportedEncodingException {
// String --> byte[]
String str = new String("域名:Jermyn");
// 使用默认的字符集进行编码,跟自己的设置有关,这个是UTF-8
byte[] bytes = str.getBytes();
System.out.println(Arrays.toString(bytes));
// 指定使用 GBK 进行编码
byte[] gbks = str.getBytes("GBK");
System.out.println(Arrays.toString(gbks));
// byte[]-->String
// 编解码的字符类型一致
String s = new String(gbks, "GBK");
String s1 = new String(bytes, "UTF-8");
System.out.println(s);
System.out.println(s1);
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13D:\PATH-EN\java-path\bin\java.exe ...
[-27, -97, -97, -27, -112, -115, -17, -68, -102, 74, 101, 114, 109, 121, 110]
[-45, -14, -61, -5, -93, -70, 74, 101, 114, 109, 121, 110]
域名:Jermyn
域名:Jermyn
===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
字符串相关的类:StringBuffer
- java.lang.StringBuffer代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删,此时不会产生新的对象。
- 很多方法与String相同。
- 作为参数传递时,方法内部可以改变值(可变)。
- StringBuffer类不同于String,其对象必须使用构造器生成。有三个构造器:
- StringBuffer():初始容量为16的字符串缓冲区
- StringBuffer(int size):构造指定容量的字符串缓冲区
- StringBuffer(String str):将内容初始化为指定字符串内容
字符串相关的类:StringBuilder
StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且 提供相关功能的方法也一样StringBuffer类的常用方法
- StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接
- StringBuffer delete(int start,int end):删除指定位置的内容
- StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
- StringBuffer insert(int offset, xxx):在指定位置插入xxx StringBuffer reverse() :把当前字符序列逆转
- public int indexOf(String str)
public String substring(int start,int end)
public int length()
public char charAt(int n )
public void setCharAt(int n ,char ch)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68import org.testng.annotations.Test;
public class StringBufferAndBuilderTest {
public void test01() {
// 1.StringBuffer append(xxx):提供了很多的append()方法,用于进行字符串拼接
StringBuffer stringBuffer = new StringBuffer("Jermyn");
StringBuffer stringBuffer01 = stringBuffer.append(".cn");
System.out.println(stringBuffer01);
}
public void test02() {
// 2. StringBuffer delete(int start,int end):删除指定位置的内容
StringBuffer stringBuffer02 = new StringBuffer("https://jermyn.cn");
StringBuffer stringBuffer03 = stringBuffer02.delete(0, 8);
System.out.println(stringBuffer03);
System.out.println(stringBuffer02);
}
public void test03() {
// 3.StringBuffer replace(int start, int end, String str):把[start,end)位置替换为str
StringBuffer stringBuffer04 = new StringBuffer("https://jermyn.cn");
StringBuffer stringBuffer05 = stringBuffer04.replace(8, 14, "JERMYN");
System.out.println(stringBuffer05);
System.out.println(stringBuffer04);
}
public void test04() {
// 4.StringBuffer insert(int offset, xxx):在指定位置插入xxx
StringBuffer stringBuffer = new StringBuffer("http://jermyn.cn");
System.out.println(stringBuffer);
StringBuffer stringBuffer01 = stringBuffer.insert(4, "s");
System.out.println(stringBuffer01);
}
public void test05() {
// 5.StringBuffer reverse() :把当前字符序列逆转
StringBuffer stringBuffer = new StringBuffer("https://jermyn.cn");
System.out.println(stringBuffer.reverse());
}
public void test06() {
StringBuffer stringBuffer = new StringBuffer("https://jermyn.cn");
System.out.println(stringBuffer.indexOf("jer"));
System.out.println(stringBuffer.substring(8, 14));
System.out.println(stringBuffer.length());
System.out.println(stringBuffer.charAt(5));
stringBuffer.setCharAt(16,'/');
System.out.println(stringBuffer);
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22D:\PATH-EN\java-path\bin\java.exe ...
Jermyn.cn
jermyn.cn
jermyn.cn
https://JERMYN.cn
https://JERMYN.cn
http://jermyn.cn
https://jermyn.cn
nc.nymrej//:sptth
8
jermyn
17
:
https://jermyn.c/
===============================================
Default Suite
Total tests run: 6, Passes: 6, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0个人理解:关于String,StringBuffer,StringBuilder,开发中建议选择StringBuffer(int capacity),capacity为数组的长度,后两者较前者是可变的 string ,底层源码显示,他们都是初始定义一个长度为16字符的数组,长度增加时就扩容,每次扩容的长度为16+(value.leng<<1)+2,就是,原来长度的二倍再加2,同时将原来的数组copy到新扩容的数组中,由于有了copy的操作,所以才有了前面说的,选择StringBuffer指定String的长度,这个长度自己预估总共的append的次数,且预估最终的长度,这样就避免了多次扩容时的copy操作导致性能的下降。是否选择StringBuilder就看处理的数据是否需要线程的安全。需要则选择后者。
- 对比String、StringBuffer、StringBuilder
- String(JDK1.0):不可变字符序列
- StringBuffer(JDK1.0):可变字符序列、效率低、线程安全
- StringBuilder(JDK 5.0):可变字符序列、效率高、线程不安全
注意:作为参数传递的话,方法内部String不会改变其值,StringBuffer和StringBuilder会改变其值。
三者之间的转化就调用目的类型的构造器。
运行效率测试
1 | public class OperationEfficiencyTest { |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
从高到低排列:StringBuilder > StringBuffer > String
JDK8之前日期时间API
- java.lang.System类
System类提供的public static long currentTimeMillis()用来返回当前时 间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差。 java.util.Date类
表示特定的瞬间,精确到毫秒构造器:
- Date():使用无参构造器创建的对象可以获取本地当前时间。
- Date(long date)
常用方法
- getTime():返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
- toString():把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天(Sun, Mon, Tue, Wed, Thu, Fri, Sat),zzz是时间标准。
- 其它很多方法都过时了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43import org.testng.annotations.Test;
import java.util.Date;
public class DateTimeTest {
public void test01() {
long l = System.currentTimeMillis();
System.out.println(l);
}
public void test02(){
Date date = new Date();
System.out.println(date.toString());
System.out.println(date.getTime());
Date date1 = new Date(14564116416L);
System.out.println(date1.toString());
System.out.println(date1.getTime());
}
public void test03(){
java.sql.Date date = new java.sql.Date(35235325L);
System.out.println(date);
//如何将java.util.Date对象转换为java.sql.Date对象
//情况一:强转
Date date1 = new java.sql.Date(2343243242323L);
java.sql.Date date5 = (java.sql.Date) date1;
//情况二:
Date date2 = new Date();
java.sql.Date date3 = new java.sql.Date(date2.getTime());
System.out.println(date3);
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16D:\PATH-EN\java-path\bin\java.exe ...
1665548341186
Wed Oct 12 12:19:01 CST 2022
1665548341188
Thu Jun 18 21:35:16 CST 1970
14564116416
1970-01-01
2022-10-12
===============================================
Default Suite
Total tests run: 3, Passes: 3, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
3. java.text.SimpleDateFormat类
- Date类的API不易于国际化,大部分被废弃了,java.text.SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类。
- 它允许进行格式化:日期—>文本、解析:文本—>日期
格式化:
- SimpleDateFormat() :默认的模式和语言环境创建对象
- public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个对象,该对象调用:
- public String format(Date date):方法格式化时间对象date
解析:
- public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateTimeTest {
public static void main(String[] args) throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
// 1.格式化 日期 --> 字符串
Date date = new Date();
// date 输出相当于调用了 toString 方法
System.out.println(date);
// 默认的格式化输出
String format = simpleDateFormat.format(date);
System.out.println(format);
// 2. 解析 字符串 --> 日期
String str = "22-10-12 下午5:12";
// 显然解析的过程,会与格式有关,所以避免报错,必须异常处理。
Date parse = simpleDateFormat.parse(str);
System.out.println(parse);
// // 错误
// String s = "22-10-12 17:18";
// Date parse1 = simpleDateFormat.parse(s);
// System.out.println(parse1);
//*************按照指定的方式格式化和解析:调用带参的构造器*****************
SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// 格式化
String format1 = simpleDateFormat1.format(date);
System.out.println(format1);
// 解析
Date parse1 = simpleDateFormat1.parse(format1);
System.out.println(parse1);
Date parse2 = simpleDateFormat1.parse("1999-11-15 00:00:00");
System.out.println(parse2);
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9D:\PATH-EN\java-path\bin\java.exe ...
Wed Oct 12 17:48:27 CST 2022
22-10-12 下午5:48
Wed Oct 12 17:12:00 CST 2022
2022-10-12 05:48:27
Wed Oct 12 05:48:27 CST 2022
Mon Nov 15 00:00:00 CST 1999
Process finished with exit code 0个人理解:格式化过程:①调用SimpleDateFormat的构造器,生成实例,构造器的内容时格式。②调用SimpleDateFormat的format方法,格式化时间date。解析过程:调用parse方法。
- public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期。
习题:字符串”2000-03-05”,转换为java.sql.Date
1 | import java.text.ParseException; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
4. java.util.Calendar(日历)类
- Calendar是一个抽象基类,主用用于完成日期字段之间相互操作的功能。
获取Calendar实例的方法
- 使用Calendar.getInstance()方法
- 调用它的子类GregorianCalendar的构造器。
一个Calendar的实例是系统时间的抽象表示,通过get(int field)方法来取得想 要的时间信息。比如YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY 、 MINUTE、SECOND
- public void set(int field,int value)
- public void add(int field,int amount)
- public final Date getTime()
- public final void setTime(Date date)
注意:
- 获取月份时:一月是0,二月是1,以此类推,12月是11
- 获取星期时:周日是1,周二是2 , 。。。。周六是7
1 | import org.testng.annotations.Test; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
JDK8中新日期时间API
新日期时间API出现的背景
JDK 1.0中包含了 一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:
可变性:像日期和时间这样的类应该是不可变的。
偏移性:Date中的年份是从1900开始的,而月份都从0开始。 格式化:格式化只对Date有用,Calendar则不行。
此外,它们也不是线程安全的;不能处理闰秒等。新时间日期API
- 第三次引入的API是成功的,并且Java 8中引入的java.time API 已经纠正了过去的缺陷,将来很长一段时间内它都会为我们服务。
- Java 8 吸收了 Joda-Time 的精华,以一个新的开始为Java创建优秀的API。 新的java.time 中包含了所有关于本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区(ZonedDateTime)和持续时间(Duration)的类。历史悠久的 Date 类新增了 toInstant() 方法, 用于把 Date 转换成新的表示形式。这些新增的本地化时间日期 API 大大简化了日期时间和本地化的管理。
新时间日期API
java.time – 包含值对象的基础包
java.time.chrono – 提供对不同的日历系统的访问
java.time.format – 格式化和解析时间和日期
java.time.temporal – 包括底层框架和扩展特性
java.time.zone – 包含时区支持的类
LocalDate、LocalTime、LocalDateTime
LocalDate、LocalTime、LocalDateTime 类是其中较重要的几个类,它们的实例 是不可变的对象,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。 它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区 相关的信息。- LocalDate代表IOS格式(yyyy-MM-dd)的日期,可以存储 生日、纪念日等日期。
- LocalTime表示一个时间,而不是日期。
- LocalDateTime是用来表示日期和时间的,这是一个最常用的类之一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104import org.testng.annotations.Test;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Date;
public class Jdk8DateTimeTest {
public void test01() {
Date date1 = new Date(2000, 3, 5);
System.out.println(date1);
// 偏移量
Date date = new Date(2000 - 1900, 3 - 1, 5);
System.out.println(date);
}
public void test02() {
// now()/*now(ZoneId zone):静态方法,根据当前时间创建对象/指定时区的对象。
LocalDate now1 = LocalDate.now();
System.out.println(now1);
LocalTime now2 = LocalTime.now();
System.out.println(now2);
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
}
public void test03() {
// of():静态方法,根据指定日期/时间创建对象
LocalDateTime of = LocalDateTime.of(2000, 3, 5, 0, 0, 0);
System.out.println(of);
}
public void test04() {
// getDayOfMonth()/getDayOfYear():获得月份天数(1-31) /获得年份天数(1-366)
LocalDateTime now = LocalDateTime.now();
int dayOfMonth = now.getDayOfMonth();
DayOfWeek dayOfWeek = now.getDayOfWeek();
int dayOfYear = now.getDayOfYear();
int monthValue = now.getMonthValue();
System.out.println(dayOfMonth);
System.out.println(dayOfYear);
System.out.println(dayOfWeek);
System.out.println(monthValue);
}
public void test05() {
//withDayOfMonth()/withDayOfYear()/withMonth()/withYear():将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
LocalDate now = LocalDate.now();
System.out.println(now);
// 不可变性,修改不改变原来的数据
LocalDate localDate = now.withMonth(5);
System.out.println(localDate);
// 设置年份
LocalDate localDate1 = now.withYear(2000);
System.out.println(localDate1);
}
public void test07() {
// plusDays(), plusWeeks(),plusMonths(),plusYears(),plusHours():
// 向当前对象添加几天、几周、几个月、几年、几小时
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
LocalDateTime localDateTime = now.plusDays(5);
System.out.println(localDateTime);
}
public void test08() {
// minusMonths() / minusWeeks()/minusDays()/minusYears()/minusHours()
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
LocalDateTime localDateTime = now.minusDays(2);
System.out.println(localDateTime);
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26D:\PATH-EN\java-path\bin\java.exe ...
Thu Apr 05 00:00:00 CST 3900
Sun Mar 05 00:00:00 CST 2000
2022-10-13
12:32:53.852
2022-10-13T12:32:53.852
2000-03-05T00:00
13
286
THURSDAY
10
2022-10-13
2022-05-13
2000-10-13
2022-10-13T12:32:53.855
2022-10-18T12:32:53.855
2022-10-13T12:32:53.856
2022-10-11T12:32:53.856
===============================================
Default Suite
Total tests run: 7, Passes: 7, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
瞬时:Instant
- Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。
- 在处理时间和日期的时候,我们通常会想到年,月,日,时,分,秒。然而,这只是 时间的一个模型,是面向人类的。第二种通用模型是面向机器的,或者说是连 续的。在此模型中,时间线中的一个点表示为一个很大的数,这有利于计算机 处理。在UNIX中,这个数从1970年开始,以秒为的单位;同样的,在Java中, 也是从1970年开始,但以毫秒为单位。
- java.time包通过值类型Instant提供机器视图,不提供处理人类意义上的时间 单位。Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。 概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒 数。因为java.time包是基于纳秒计算的,所以Instant的精度可以达到纳秒级。
- (1ns =10-9s) 1秒=1000毫秒 =10^6微秒=10^9纳秒
相关方法
- now():静态方法,返回默认UTC时区的Instant类的对象
- ofEpochMilli(long epochMilli):静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒 数之后的Instant类的对象
- atOffset(ZoneOffset offset):结合即时的偏移来创建一个 OffsetDateTime
- toEpochMilli():返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
public class InstantTest {
public static void main(String[] args) {
// 静态方法,返回默认UTC时区的Instant类的对象
Instant now = Instant.now();
System.out.println(now);
// 结合即时的偏移来创建一个 OffsetDateTime
OffsetDateTime offsetDateTime = now.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);
// 静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象
long l = now.toEpochMilli();
System.out.println(l);
// 静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒 数之后的Instant类的对象静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒 数之后的Instant类的对象
Instant instant = Instant.ofEpochMilli(1502643933071L);
System.out.println(instant);
}
}点击查看运行结果
1
2
3
4
5
6
7D:\PATH-EN\java-path\bin\java.exe ...
2022-10-13T05:13:35.378Z
2022-10-13T13:13:35.378+08:00
1665638015378
2017-08-13T17:05:33.071Z
Process finished with exit code 0
格式化与解析日期或时间
java.time.format.DateTimeFormatter 类:该类提供了三种格式化方法:- 预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
- 本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)
- 自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
相关方法
ofPattern(String pattern):静态方法 , 返回一个指定字符串格式的DateTimeFormatter
format(TemporalAccessor t):格式化一个日期、时间,返回字符串
parse(CharSequence text):将指定格式的字符序列解析为一个日期、时间1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74import org.testng.annotations.Test;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.TemporalAccessor;
public class DateTimeFormatterTest {
DateTimeFormatter instance = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
DateTimeFormatter instance01 = DateTimeFormatter.ISO_LOCAL_DATE;
DateTimeFormatter instance02 = DateTimeFormatter.ISO_LOCAL_TIME;
public void test01() {
// 格式化:日期-->字符串
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
String format = instance.format(now);
System.out.println(format);
}
public void test02() {
// 解析:字符串-->日期
// 方式一:
String s = new String("2022-10-13T13:35:33.634");
TemporalAccessor parse = instance.parse(s);
System.out.println(parse);
// 方式二:
LocalDateTime now = LocalDateTime.now();
// 本地化相关的格式。如:ofLocalizedDateTime()
// FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :
// 适用于LocalDateTime
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
String format = dateTimeFormatter.format(now);
System.out.println(format);
DateTimeFormatter dateTimeFormatter01 = DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG);
String format1 = dateTimeFormatter01.format(now);
System.out.println(format1);
// 本地化相关的格式。如:ofLocalizedDate()
// FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :
// 适用于LocalDate
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
String format2 = dateTimeFormatter1.format(now);
System.out.println(format2);
}
public void test03() {
// 方式三:
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
// 格式化
String format = dateTimeFormatter.format(LocalDateTime.now());
System.out.println(format);
// 解析
TemporalAccessor parse = dateTimeFormatter.parse(format);
System.out.println(parse);
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17D:\PATH-EN\java-path\bin\java.exe ...
2022-10-13T16:04:59.066
2022-10-13T16:04:59.066
{},ISO resolved to 2022-10-13T13:35:33.634
下午4:04
下午04时04分59秒
2022-10-13
2022-10-13 04:04:59
{NanoOfSecond=0, MilliOfSecond=0, MinuteOfHour=4, SecondOfMinute=59, HourOfAmPm=4, MicroOfSecond=0},ISO resolved to 2022-10-13
===============================================
Default Suite
Total tests run: 3, Passes: 3, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
Java比较器
在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题。
Java实现对象排序的方式有两种:
- 自然排序:java.lang.Comparable
- 定制排序:java.util.Comparator
- 方式一:自然排序:java.lang.Comparable
- Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称 为类的自然排序。
- 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即 通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象this大 于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回 负整数,如果当前对象this等于形参对象obj,则返回零。
- 实现Comparable接口的对象列表(和数组)可以通过 Collections.sort 或 Arrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有 序集合中的元素,无需指定比较器。
- 对于类 C 的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与 e1.equals(e2) 具有相同的 boolean 值时,类 C 的自然排序才叫做与 equals 一致。建议(虽然不是必需的)最好使自然排序与 equals 一致。
- Comparable 的典型实现:(默认都是从小到大排列的)
- String:按照字符串中字符的Unicode值进行比较
- Character:按照字符的Unicode值来进行比较
- 数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
- Boolean:true 对应的包装类实例大于 false 对应的包装类实例
- Date、Time等:后面的日期时间比前面的日期时间大
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52import java.util.Arrays;
public class CompareTest {
public static void main(String[] args) {
Goods[] all = new Goods[4];
all[0] = new Goods("《红楼梦》", 120);
all[1] = new Goods("《水浒传》", 100);
all[2] = new Goods("《西游记》", 150);
all[3] = new Goods("《三国演义》", 80);
Arrays.sort(all);
System.out.println(Arrays.toString(all));
}
}
class Goods implements Comparable {
private String name;
private double price;
public Goods(String name, double price) {
this.name = name;
this.price = price;
}
public String toString() {
return "Goods{" +
"name='" + name + '\'' +
", price=" + price +
'}'+"\n";
}
public int compareTo(Object o) {
if (o instanceof Goods) {
Goods other = (Goods) o;
if (this.price > other.price) {
return 1;
} else if (this.price < other.price) {
return -1;
}
return 0;
}
throw new RuntimeException("输入的数据类型不一致");
}
}点击查看运行结果
1
2
3
4
5
6
7
8D:\PATH-EN\java-path\bin\java.exe ...
[Goods{name='《三国演义》', price=80.0}
, Goods{name='《水浒传》', price=100.0}
, Goods{name='《红楼梦》', price=120.0}
, Goods{name='《西游记》', price=150.0}
]
Process finished with exit code 0
- 方式二:定制排序:java.util.Comparator
- 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码, 或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序,强行对多个对象进行整体排 序的比较。
- 重写compare(Object o1,Object o2)方法,比较o1和o2的大小:如果方法返 回正整数,则表示o1大于o2;如果返回0,表示等;返回负整数,表示 o1小于o2。
- 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort), 从而允许在排序顺序上实现精确控制。
- 还可以使用 Comparator 来控制某些数据结构(如有序 set或有序映射)的顺序,或者为那些没有自然顺序的对象 collection 提供排序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77import java.util.Arrays;
import java.util.Comparator;
public class ComparatorTest {
public static void main(String[] args) {
Good[] all = new Good[4];
all[0] = new Good("《红楼梦》", 120);
all[1] = new Good("《水浒传》", 100);
all[2] = new Good("《西游记》", 150);
all[3] = new Good("《三国演义》", 80);
Arrays.sort(all, new Comparator() {
public int compare(Object o1, Object o2) {
Good g1 = (Good) o1;
Good g2 = (Good) o2;
return g1.getName().compareTo(g2.getName());
}
});
System.out.println(Arrays.toString(all));
}
}
class Good implements Comparable {
private String name;
private double price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public Good(String name, double price) {
this.name = name;
this.price = price;
}
public String toString() {
return "Goods{" +
"name='" + name + '\'' +
", price=" + price +
'}' + "\n";
}
public int compareTo(Object o) {
if (o instanceof Good) {
Good other = (Good) o;
if (this.price > other.price) {
return 1;
} else if (this.price < other.price) {
return -1;
}
return 0;
}
throw new RuntimeException("输入的数据类型不一致");
}
}点击查看运行结果
1
2
3
4
5
6
7
8D:\PATH-EN\java-path\bin\java.exe ...
[Goods{name='《三国演义》', price=80.0}
, Goods{name='《水浒传》', price=100.0}
, Goods{name='《红楼梦》', price=120.0}
, Goods{name='《西游记》', price=150.0}
]
Process finished with exit code 0
System类
- System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包。
- 由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实 例化该类。其内部的成员变量和成员方法都是static的,所以也可以很方便的进行调用。
成员变量
- System类内部包含in、out和err三个成员变量,分别代表标准输入流(键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。
成员方法
- native long currentTimeMillis():该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。
- void exit(int status):该方法的作用是退出程序。其中status的值为0代表正常退出,非零代表异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。
- void gc():该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则 取决于系统中垃圾回收算法的实现以及系统执行时的情况。
- String getProperty(String key):该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见的属性名以及属性的作用如下表所示:
1 | public class SystemTest { |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
Math类
求最大值、最小值和绝对值
求整运算
三角函数运算
指数运算
1 | import java.util.Scanner; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
BigInteger与BigDecimal
BigInteger类
- Integer类作为int的包装类,能存储的最大整型值为231-1,Long类也是有限的, 最大为263-1。如果要表示再大的整数,不管是基本数据类型还是他们的包装类 都无能为力,更不用说进行运算了。
- java.math包的BigInteger可以表示不可变的任意精度的整数。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。 另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、 位操作以及一些其他操作。
- 构造器
- BigInteger(String val):根据字符串构建BigInteger对象
BigDecimal类
- 一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中, 要求数字精度比较高,故用到java.math.BigDecimal类。
BigDecimal类支持不可变的、任意精度的有符号十进制定点数。
构造器
- public BigDecimal(double val)
- public BigDecimal(String val)
- 常用方法
- public BigDecimal add(BigDecimal augend)
- public BigDecimal subtract(BigDecimal subtrahend)
- public BigDecimal multiply(BigDecimal multiplicand)
- public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
第三章 枚举类与注解
枚举类的使用
- 枚举类的实现
- JDK1.5之前需要自定义枚举类
- JDK 1.5 新增的 enum 关键字用于定义枚举类
- 若枚举只有一个对象, 则可以作为一种单例模式的实现方式。
枚举类的属性
- 枚举类对象的属性不应允许被改动, 所以应该使用 private final 修饰
- 枚举类的使用 private final 修饰的属性应该在构造器中为其赋值
- 若枚举类显式的定义了带参数的构造器, 则在列出枚举值时也必须对应的传入参数
自定义枚举类定义步骤:
- 声明对象的属性:private final修饰
- 私有化类的构造器,并给对象属性赋值
- 提供当前枚举类的多个对象:public static final的
- 其他诉求1:获取枚举类对象的属性
- 其他诉求2:提供toString()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53public class EnumTest {
public static void main(String[] args) {
Season spring = Season.SPRING;
Season summer = Season.SUMMER;
Season autumn = Season.AUTUMN;
Season winter = Season.WINTER;
System.out.println(spring);
System.out.println(summer);
System.out.println(autumn);
System.out.println(winter);
}
}
class Season {
//1.声明Season对象的属性:private final修饰
private final String seasonName;
private final String seasonDesc;
//2.私有化类的构造器,并给对象属性赋值
private Season(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//3.提供当前枚举类的多个对象:public static final的
public static final Season SPRING = new Season("春天", "春暖花开");
public static final Season SUMMER = new Season("夏天", "夏日炎炎");
public static final Season AUTUMN = new Season("秋天", "秋高气爽");
public static final Season WINTER = new Season("冬天", "冰天雪地");
//4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
//4.其他诉求2:提供toString()
public String toString() {
return "Season{" +
"seasonName='" + seasonName + '\'' +
", seasonDesc='" + seasonDesc + '\'' +
'}';
}
}点击查看运行结果
1
2
3
4
5
6
7D:\PATH-EN\java-path\bin\java.exe ...
Season{seasonName='春天', seasonDesc='春暖花开'}
Season{seasonName='夏天', seasonDesc='夏日炎炎'}
Season{seasonName='秋天', seasonDesc='秋高气爽'}
Season{seasonName='冬天', seasonDesc='冰天雪地'}
Process finished with exit code 0
- 使用enum定义枚举类
使用说明
- 使用 enum 定义的枚举类默认继承了java.lang.Enum类,因此不能再继承其他类
- 枚举类的构造器只能使用private权限修饰符
- 枚举类的所有实例必须在枚举类中显式列出 ((, 分隔 ; 结尾 。列出的实例系统会自动添加 public static final 修饰
- 必须在枚举类的第一行声明枚举类对象
JDK 1.5 中可以在 switch 表达式中使用 Enum 定义的枚举类的对象作为表达式 , case 子句可以直接使用枚举值的名字 , 无需添加枚举类作为限定。
- 使用enum定义枚举类定义步骤:
- 提供当前枚举类的对象,多个对象之间用”,”隔开,末尾对象”;”结束
- 声明对象的属性:private final修饰
- 私有化类的构造器,并给对象属性赋值
- 其他诉求1:获取枚举类对象的属性
- 其他诉求2.如果需要的重写toString()方法,默认返回常量的名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50public class EnumTest {
public static void main(String[] args) {
Season spring = Season.SPRING;
Season summer = Season.SUMMER;
Season autumn = Season.AUTUMN;
Season winter = Season.WINTER;
System.out.println(spring);
System.out.println(summer);
System.out.println(autumn);
System.out.println(winter);
}
}
enum Season {
// 1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
SPRING("春天", "春暖花开"),
SUMMER("夏天", "夏日炎炎"),
AUTUMN("秋天", "秋高气爽"),
WINTER("冬天", "冰天雪地");
// 2.声明Season对象的属性:private final修饰
private final String seasonName;
private final String seasonDesc;
// 3.私有化类的构造器,并给对象属性赋值
private Season(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
// 4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
// 5.其他诉求2.如果需要的重写toString()方法,默认返回常量的名
public String toString() {
return getSeasonName();
}
}点击查看运行结果
1
2
3
4
5
6
7D:\PATH-EN\java-path\bin\java.exe ...
春天
夏天
秋天
冬天
Process finished with exit code 0
- Enum类的主要方法
1 name 返回此枚举常量的名称,在其枚举声明中对其进行声明。 与此方法相比,大多数程序员应该优先考虑使用 toString() 方法,因为 toString 方法返回更加用户友好的名称。该方法主要设计用于特殊情形,其正确性取决于获取正确的名称,其名称不会随版本的改变而改变。 2 ordinal 返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)。 大多数程序员不会使用此方法。它被设计用于复杂的基于枚举的数据结构,比如 EnumSet 和 EnumMap。 3 toString 返回枚举常量的名称,它包含在声明中。可以重写此方法,虽然一般来说没有必要。当存在更加“程序员友好的”字符串形式时,应该使用枚举类型重写此方法。 4 equals 当指定对象等于此枚举常量时,返回 true。 5 hashCode 返回枚举常量的哈希码。 6 clone 抛出 CloneNotSupportedException。这可保证永远不会复制枚举,这对于保留其“单元素”状态是必需的。 7 compareTo 比较此枚举与指定对象的顺序。在该对象小于、等于或大于指定对象时,分别返回负整数、零或正整数。 枚举常量只能与相同枚举类型的其他枚举常量进行比较。该方法实现的自然顺序就是声明常量的顺序。 8 getDeclaringClass 返回与此枚举常量的枚举类型相对应的 Class 对象。当且仅当 e1.getDeclaringClass() == e2.getDeclaringClass() 时,两个枚举常量 e1 和 e2 的枚举类型才相同。(由该方法返回的值不同于由 Object.getClass() 方法返回的值,Object.getClass() 方法用于带有特定常量的类主体的枚举常量。) 9 valueOf 返回带指定名称的指定枚举类型的枚举常量。名称必须与在此类型中声明枚举常量所用的标识符完全匹配。(不允许使用额外的空白字符。)
1 | public class EnumMethodTest { |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
- 实现接口的枚举类
- 和普通 Java 类一样,枚举类可以实现一个或多个接口
- 若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
- 若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59public class EnumFlagTest {
public static void main(String[] args) {
Flag aTrue = Flag.TRUE;
Flag aFalse = Flag.FALSE;
System.out.println(aTrue);
System.out.println(aFalse);
Flag[] values = Flag.values();
for (Flag flag:values) {
flag.show();
}
}
}
enum Flag implements Info{
TRUE("TRUE", "is true"){
public void show() {
System.out.println("That is true!");
}
},
FALSE("FALSE", "is false") {
public void show() {
System.out.println("That is false!");
}
};
private final String flagName;
private final String flagDesc;
Flag(String flagName, String flagDesc) {
this.flagName = flagName;
this.flagDesc = flagDesc;
}
public String getFlagName() {
return flagName;
}
public String getFlagDesc() {
return flagDesc;
}
public String toString() {
return flagName;
}
}
interface Info {
void show();
}点击查看运行结果
1
2
3
4
5
6
7D:\PATH-EN\java-path\bin\java.exe ...
TRUE
FALSE
That is true!
That is false!
Process finished with exit code 0
注解
注解 (Annotation) 概述
- 从 JDK 5.0 开始, Java 增加了对元数据(MetaData) 的支持, 也就是Annotation(注解)
- Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加 载, 运行时被读取, 并执行相应的处理。通过使用 Annotation, 程序员 可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。代 码分析工具、开发工具和部署工具可以通过这些补充信息进行验证 或者进行部署。
- Annotation 可以像修饰符一样被使用, 可用于修饰包,类, 构造器, 方 法, 成员变量, 参数, 局部变量的声明, 这些信息被保存在 Annotation 的 “name=value” 对中。
- 在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE/Android中注解占据了更重要的角色,例如 用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗 代码和XML配置等。
- 未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以 上都是基于注解的,Hibernate3.x以后也是基于注解的,现在的 Struts2有一部分也是基于注解的了,注解是一种趋势,一定程度上 可以说:框架 = 注解 + 反射 + 设计模式。
- 使用 Annotation 时要在其前面增加 @ 符号, 并把该Annotation 当成一个修饰符使用。用于修饰它支持的程序元素
自定义Annotation
- 定义新的Annotation 类型使用 @interface 关键字
- 自定义注解自动继承了java.lang.annotation.Annotation接口
- Annotation 的成员变量在 Annotation 定义中以无参数方法的形式来声明。其方法名和返回值定义了该成员的名字和类型。我们称为配置参数。类型只能 是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、 以上所有类型的数组。
- 可以在定义 Annotation 的成员变量时为其指定初始值, 指定成员变量的初始值可使用 default 关键字
- 如果只有一个参数成员,建议使用参数名为value
- 如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认 值。格式是“参数名 = 参数值”,如果只有一个参数成员,且名称为value,可以省略“value=”
- 没有成员定义的 Annotation 称为标记; 包含成员变量的 Annotation 称为元数据Annotation
注意:自定义注解必须配上注解的信息处理流程才有意义。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import java.lang.annotation.*;
public class AnnotationTest {
public static void main(String[] args) {
Class clazz = AnnotationTest.class;
Annotation a = clazz.getAnnotation(MyAnnotation.class);
MyAnnotation m = (MyAnnotation) a;
String info = m.value();
System.out.println(info);
}
}
// 1.注解声明为:@interface
MyAnnotation {
// 2.内部定义成员,通常使用value表示
// 3.可以指定成员的默认值,使用default定义
String value() default "JERMYN";
}点击查看运行结果
1
2
3
4D:\PATH-EN\java-path\bin\java.exe ...
Jermyn
Process finished with exit code 0
- JDK 中的元注解
- JDK 的元Annotation 用于修饰其他Annotation 定义
JDK5.0提供了4个标准的meta-annotation类型,分别是:
- Retention
- Target
- Documented
- Inherited
个人理解:元注解就是对现有注解进行解释说明的注解,类比 String name = “Jermyn”;String 是数据类型,name是数据的名称,”Jermyn”就是数据,String和name就是元数据,”Jermyn”就是数据。
@Retention
- @Retention: 只能用于修饰一个 Annotation 定义, 用于指定该 Annotation 的生命 周期, @Rentention 包含一个 RetentionPolicy 类型的成员变量, 使用 @Rentention 时必须为该 value 成员变量指定值:
- RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),编译器直接丢弃这种策略的注释
- RetentionPolicy.CLASS:在class文件中有效(即class保留) , 当运行 Java 程序时, JVM不会保留注解。 这是默认值
- RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当运行 Java 程序时, JVM 会保留注释。程序可以通过反射获取该注释。
点击图片可以跳转源码
1 |
|
个人理解:RetentionPolicy修饰注解的生命周期就是什么时候”死”,RetentionPolicy的三种状态,SOURCE,ClASS,RUNTIME,简单理解为以此来修饰的注解的生命周期,在某种状态时可以被使用,即再某种状态后会被抛弃,还可以理解为,如果修饰的注解在运行时还会起作用就不能用前两种修饰。如果在编译的时候起作用,运行时无关紧要就可以用CLASS修饰,所以如果想要反射时可以检测到,就用RUNTIME。
@Target:
@Target: 用于修饰 Annotation 定义, 用于指定被修饰的 Annotation 能用于修饰哪些程序元素。 @Target 也包含一个名为 value 的成员变量。
点击图片可以跳转源码- value的取值
点击图片可以跳转源码
- value的取值
1 |
|
个人理解:元注解Target就是解释注解可以修饰那些元素,即写在那些元素的上面
其余两个元注解用较少
@Documented: 用于指定被该元 Annotation 修饰的 Annotation 类将被javadoc 工具提取成文档。默认情况下,javadoc是不包括注解的。定义为Documented的注解必须设置Retention值为RUNTIME。
@Inherited: 被它修饰的 Annotation 将具有继承性。如果某个类使用了被@Inherited 修饰的 Annotation, 则其子类将自动具有该注解。
- 比如:如果把标有@Inherited注解的自定义的注解标注在类级别上,子类则可以继承父类类级别的注解
- 实际应用中,使用较少
第四章 Java集合
Java集合框架概述
一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array存储对象方面具有一些弊 端,而Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中。
数组在内存存储方面的特点:
- 数组初始化以后,长度就确定了。
- 数组声明的类型,就决定了进行元素初始化时的类型
数组在存储数据方面的弊端:
- 数组初始化以后,长度就不可变了,不便于扩展
- 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储元素的个数
- 数组存储的数据是有序的、可以重复的。——>存储数据的特点单一
Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的 关联数组。
Java 集合可分为 Collection 和 Map 两种体系
Collection接口:单列数据,定义了存取一组对象的方法的集合
- List:元素有序、可重复的集合
- Set:元素无序、不可重复的集合
Map接口:双列数据,保存具有映射关系“key-value对”的集合
Collection接口方法
- Collection 接口
- Collection接口是List、Set和Queue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和Queue集合。
- JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。
- 在Java5之前,Java集合会丢失容器中所有对象的数据类型,把所有对象都当成Object类型处理从JDK5.0增加了泛型以后,Java 集合可以记住容器中对象的数据类型。
- 接口方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177import org.junit.Test;
import java.util.*;
public class CollectionTest {
Collection collection = new ArrayList();
Collection collection01 = new ArrayList();
public void addTest() {
// add(Object e),添加的数据可以时任何类型
collection.add("Jermyn");
collection.add('c');
collection.add(123);
collection.add(new Object());
System.out.println(collection.size());
// addAll()
collection01.add(new Date());
collection01.addAll(collection);
System.out.println(collection.size());
System.out.println(collection01.size());
System.out.println(collection01.toString());
}
public void isEmptyTest() {
// 判断集合是否为空
System.out.println(collection.isEmpty());
collection.add("jermyn");
System.out.println(collection.isEmpty());
}
public void clearTest() {
collection.add("jermyn");
collection.add("https://jermyn.cn/");
System.out.println(collection.isEmpty());
// 清空集合
collection.clear();
System.out.println(collection.isEmpty());
}
public void containsTest() {
collection.add("jermyn");
collection.add("https://jermyn.cn/");
collection.add(new String("JERMYN"));
// 是否包含obj
System.out.println(collection.contains("Jermyn"));
System.out.println(collection.contains("jermyn"));
// 结果为true,则比较的是内容,显然地址值是不同,实际是上是调用 equals 方法
// 如果自己定义的类,然后比较的实例是否包含,类比String可知是调用 equals 方法,
// 但是如果自己定义的类没有重写equals方法,就是调用Object的equals方法,而Object的equals
// 方法就是"=="比较的是地址值,所以是false,如果重写equals则是true
System.out.println(collection.contains(new String("JERMYN")));
Collection collection01 = Arrays.asList("jermyn",
"https://jermyn.cn/",
new String("JERMYN"),
25);
// containsAll:判断collection中的数据是否都在collection01 中
System.out.println(collection01.containsAll(collection));
collection.add("jer");
System.out.println(collection01.containsAll(collection));
}
public void removeTest() {
collection.add("Jermyn");
collection.add('c');
collection.add(123);
collection.add(new Object());
// remove(Object obj): 找到需要移除的数据返回true,反之返回false
System.out.println(collection.remove("jer"));
System.out.println(collection);
System.out.println(collection.remove("Jermyn"));
System.out.println(collection);
// removeAll(Collection c):从当前集合中移除 c 中所有的元素
// 类比 containsAll,不演示
}
public void retainAll() {
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(789);
coll.add(false);
//5.retainAll(Collection coll1):交集:获取当前集合和coll1集合的交集,并返回给当前集合
Collection coll1 = Arrays.asList(123, 456, 789);
coll.retainAll(coll1);
System.out.println(coll);
}
public void equalsTest() {
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
//equals(Object obj):要想返回true,需要当前集合和形参集合的元素都相同。
Collection coll1 = new ArrayList();
coll1.add(456);
coll1.add(123);
coll1.add(new String("Tom"));
coll1.add(false);
System.out.println(coll.equals(coll1));
}
public void hashCodeTest() {
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
// hashCode():返回当前对象的哈希值
System.out.println(coll.hashCode());
}
public void toArrayTest() {
collection.add(123);
collection.add("Jermyn");
collection.add('j');
collection.add(new String("JERMYN"));
collection.add(new Date());
// toArray():将集合转换为数组,因为集合都是Object 的实例,所以集合就是Object[]
// 集合-->数组
Object[] objects = collection.toArray();
for (Object obj : objects) {
System.out.print(obj + ",");
}
// 数组-->集合
// 调用Arrays类的静态方法asList()
List list = Arrays.asList("Jermyn", 'j', new Date(), new String("JERMYN"));
System.out.println();
System.out.println(list);
}
public void iteratorTest() {
// iterator():返回Iterator接口的实例,用于遍历集合元素。放在IteratorTest.java中测试
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25D:\PATH-EN\java-path\bin\java.exe ...
false
true
true
true
false
123,Jermyn,j,JERMYN,Fri Oct 14 19:42:14 CST 2022,
[Jermyn, j, Fri Oct 14 19:42:14 CST 2022, JERMYN]
[123, 456, 789]
false
true
4
4
5
[Fri Oct 14 19:42:14 CST 2022, Jermyn, c, 123, java.lang.Object@61e717c2]
true
false
false
false
[Jermyn, c, 123, java.lang.Object@1b9e1916]
true
[c, 123, java.lang.Object@1b9e1916]
7639761
Process finished with exit code 0
Iterator迭代器接口
使用 Iterator 接口遍历集合元素
- Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元 素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公 交车上的售票员”、“火车上的乘务员”、“空姐”。
- Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所 有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了 Iterator接口的对象。
- Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
- 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合 的第一个元素之前。
Iterator方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorTest {
// iterator():返回Iterator接口的实例,用于遍历集合元素。
public void hasNextTest() {
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(new String("Tom"));
collection.add(false);
// 方式一:
System.out.println("*******************方式一*******************");
Iterator iterator = collection.iterator();
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
// 报错
// System.out.println(iterator.next());
// 方式二:
System.out.println("*******************方式二*******************");
Iterator iterator02 = collection.iterator();
for (int i = 0; i < collection.size(); i++) {
System.out.println(iterator02.next());
}
//方式三:
System.out.println("*******************方式三*******************");
Iterator iterator03 = collection.iterator();
while (iterator03.hasNext()) {
//next():①指针下移 ②将下移以后集合位置上的元素返回
System.out.println(iterator03.next());
}
}
public void removeTest() {
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(new String("Tom"));
collection.add(false);
Iterator iterator = collection.iterator();
// 删除集合中的"Tom"
while (iterator.hasNext()) {
Object obj = iterator.next();
if ("Tom".equals(obj)) {
iterator.remove();
}
}
// 遍历集合
Iterator iterator01 = collection.iterator();
while (iterator01.hasNext()) {
System.out.println(iterator01.next());
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22D:\PATH-EN\java-path\bin\java.exe ...
*******************方式一*******************
123
456
Tom
false
*******************方式二*******************
123
456
Tom
false
*******************方式三*******************
123
456
Tom
false
*******************removeTest*******************
123
456
false
Process finished with exit code 0使用 foreach 循环遍历集合元素
- Java 5.0 提供了 foreach 循环迭代访问 Collection和数组。
- 遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
- 遍历集合的底层调用Iterator完成操作。
- foreach还可以用来遍历数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29import java.util.ArrayList;
import java.util.Collection;
public class ForEachTest {
public static void main(String[] args) {
// 示例一
int[] arr = new int[]{1, 2, 3, 4, 5, 6};
//for(数组元素的类型 局部变量 : 数组对象)
for (int i : arr) {
System.out.println(i);
}
// 示例二
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);
//for(集合元素的类型 局部变量 : 集合对象)
//内部仍然调用了迭代器。
for(Object obj : coll){
System.out.println(obj);
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13D:\PATH-EN\java-path\bin\java.exe ...
1
2
3
4
5
6
123
456
Tom
false
Process finished with exit code 0
Collection子接口之一:List接口
List接口概述
- 鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
- List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据 序号存取容器中的元素。
- JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。
List接口方法
- List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
- void add(int index, Object ele):在index位置插入ele元素
- boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
- Object get(int index):获取指定index位置的元素
- int indexOf(Object obj):返回obj在集合中首次出现的位置
- int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
- Object remove(int index):移除指定index位置的元素,并返回此元素
- Object set(int index, Object ele):设置指定index位置的元素为ele
- List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116import org.junit.Test;
import java.util.*;
public class ListTest {
public void test01() {
ArrayList arrayList = new ArrayList();
arrayList.add(123);
arrayList.add("");
arrayList.add("Jermyn");
arrayList.add(new Date());
System.out.println(arrayList);
System.out.println("***************************************");
// add(index,element):在index之后插入element
arrayList.add(1, "JERMYN");
System.out.println(arrayList);
List integers = Arrays.asList(1, 2, 3);
// addAll(int index,Collection c):从index位置开始将c中的所有元素添加进来
boolean b = arrayList.addAll(1, integers);
System.out.println(arrayList.size());
// get(int index):获取指定index位置的元素
Object o = arrayList.get(6);
System.out.println(o);
}
public void test02() {
ArrayList arrayList = new ArrayList();
arrayList.add(123);
arrayList.add("");
arrayList.add("Jermyn");
arrayList.add(123);
arrayList.add(new Date());
System.out.println(arrayList);
// int indexOf(Object obj):返回obj在集合中首次出现的位置,有返回index
int i = arrayList.indexOf("");
System.out.println(i);
// 没有返回-1
int index = arrayList.indexOf("JERMYN");
System.out.println(index);
// int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置,没有返回-1
int index1 = arrayList.lastIndexOf(123);
System.out.println(index1);
// Object remove(int index):移除指定index位置的元素,并返回此元素(重载了Collection的remove方法)
Object remove = arrayList.remove(4);
System.out.println(arrayList);
// remove(Object o):Collection的remove删除指定元素
boolean remove1 = arrayList.remove("Jermyn");
System.out.println(arrayList);
// Object set(int index, Object ele):设置指定index位置的元素为ele
Object set = arrayList.set(1, "https://jermyn.cn");
System.out.println(arrayList);
arrayList.add(1);
arrayList.add(13);
arrayList.add(12);
// List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
List list = arrayList.subList(2, 5);
System.out.println(arrayList);
System.out.println(list);
}
public void test03() {
ArrayList arrayList = new ArrayList();
arrayList.add(123);
arrayList.add("");
arrayList.add("Jermyn");
arrayList.add(123);
arrayList.add(new Date());
arrayList.add(1);
arrayList.add(13);
arrayList.add(12);
// 遍历方式一:Iterator迭代器方式
Iterator iterator = arrayList.iterator();
System.out.print("方式一:");
while (iterator.hasNext()) {
System.out.print(iterator.next() + ",");
}
// 遍历方式二:增强for循环
System.out.println();
System.out.print("方式二:");
for (Object o : arrayList) {
System.out.print(o + ",");
}
// 遍历方式三:普通for循环
System.out.println();
System.out.print("方式三:");
for (int i = 0; i < arrayList.size(); i++) {
System.out.print(arrayList.get(i) + ",");
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19D:\PATH-EN\java-path\bin\java.exe ...
[123, , Jermyn, Sat Oct 15 18:13:24 CST 2022]
***************************************
[123, JERMYN, , Jermyn, Sat Oct 15 18:13:24 CST 2022]
8
Jermyn
[123, , Jermyn, 123, Sat Oct 15 18:13:24 CST 2022]
1
-1
3
[123, , Jermyn, 123]
[123, , 123]
[123, https://jermyn.cn, 123]
[123, https://jermyn.cn, 123, 1, 13, 12]
[123, 1, 13]
方式一:123,,Jermyn,123,Sat Oct 15 18:13:24 CST 2022,1,13,12,
方式二:123,,Jermyn,123,Sat Oct 15 18:13:24 CST 2022,1,13,12,
方式三:123,,Jermyn,123,Sat Oct 15 18:13:24 CST 2022,1,13,12,
Process finished with exit code 0小练习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class ListExer {
public void testListRemove() {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list);
System.out.println(list);//
}
private static void updateList(List list) {
list.remove(2);
list.remove(new Integer(1));
}
}点击查看运行结果
1
2
3
4D:\PATH-EN\java-path\bin\java.exe ...
[2]
Process finished with exit code 0
- List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
List实现类之一:ArrayList
- ArrayList 是 List 接口的典型实现类、主要实现类本质上,ArrayList是对象引用的一个”变长”数组
ArrayList的JDK1.8之前与之后的实现区别?
- JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
- JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元 素时再创建一个始容量为10的数组
Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合
JDK1.7和JDK1.8的扩容过程
个人理解,有错误的请指教JDK1.7的ArrayList的扩容:底层是用数组存储Object类型的数组,声明为Object[] elementDate,创建当前
ArrayList list = new ArrayList 对象,使用new + 类的构造器创建。初始化当前的底层的数组, 数组的容量即initialCapacity=10,然后进行添加操作调用 add() 方法,添加的内容就是Object类型的数据,添加之前先确定数组的容量是否够,即是否能加进去,源码显示ensureCapacityInternal(size + 1),第一次执行add的话size就是 0。size的取值就是调用size(),这个方法返回就是add几个。前面的确认容量机制会判断add的是否大于底层初始的容量就是默认的10。如果底层数组够的话就不用扩容。一直就可以add,如果总长度大于10的话,就进行扩容,源码显示:grew(minCapacity)。此时的minCapacity就是底层数组长度+add的数组长度。扩容的操作是:先把minCapacity.length赋值给oldCapacity。然后创建新的长度(newCapacity)。这个新的长度为原来的1.5倍,源码显示为:oldCapacity+(oldCapacity >> 1),此时会执行几个判断①新扩容的是否小于刚拿来的minCapacity长度,小的话就直接把minCapacity的长度给newCapacity。②如果发现扩容1.5背还不能存储add的下一个数组,数组的长度都快到数组能够存储的最大长度(MAX_ARRAY_SIZE)了,这是就new一个数组新的数组长度,这个数组的长度使用hugeCapacity(minCapacity)来赋值,在这个方法中会判断是否是整形的能够声明的最大值。如果还不够用,就报错,这个Error叫做OutOfMemoryError。总的来说①②这两个情况很少出现,可以说几乎不会出现。仔细想就是一般不会有比整形最大值还大的数组长度了。接前面,扩充完之后就是需要将原来数组的内容copy给扩充1.5倍后的数组了。源码显示就是elementDate = Arrays.copyOf(elementDate newCapacity),至此数组的扩容就完成了。
提示:在开发中,大概可以确定数组存储多少个元素。或者可以基本确定会add多少次到达多少元素的话,我们定义的时候就不要使用空参的构造器了(空参构造器默认数组长度为10),我们使用带参的构造器,例如我预估数组长度或达到100的话就new ArrayList(100),这个100就是initialCapacity。目的是频繁的扩容会频繁的复制数组造成效率低。
JDK1.8的ArrayList的扩容:开始时我们通过空参的构造器创建对象,1.8中并没有像1.7中直接new一个长度为10的数组,而是elementDate指向一个常量即:DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这个常量的特点就是它等于{},即为空,也就是,new的时候没有直接生成数组,而是指向{}。这种方式显然比1.7的好,不用直接占用内存创建数组。那么在什么时候创建呢,就是在第一次执行add操作的时候。执行add操作的时候首先也是ensureCapacityInternal(size + 1),显然时不够,因为没有容量。进入这个方法后,首先的操作就是判断elementDate是否等于{}即DEFAULTCAPACITY_EMPTY_ELEMENTDATA,显然第一次的时候是等于的。这时会给minCapacity复制源码显示:minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity),这个DEFAULT_CAPACITY就是10。minCapacity就是0,这都是对于第一次来说。此时的minCapacity取最大的就是DEFAULT_CAPACITY即为10。下一步执行ensureExplicitCacity()方法将minCapacity传入。进入这个方法后会判断minCapacity的大小,如果比elementDate.length大,就执行grew(minCapacity),显然是比elementDate.length大的,因为第一add的时候elementDate.length为0。然后就进行扩容操作,进入grew()后,首先将oldCapacity赋值为elementDate.length即为0。newCapacity为oldCapacity+(oldCapacity >> 1),所以newCapacity也是0(0扩大1.5倍还是0)。然后执行判断,newCapacity和minCapacity的大小,newCapacity显然小于minCapacity,因为前者为0,后者为10。此时把minCapacity赋值给newCapacity,则newCapacity为10。然后就给elementDate进行copy的操作,elementDate就从原来的{}变为Arrays.copyOf(elementDate newCapacity)。成为一个数组长度为10的数组。此时数组创建完成。如果扩容的话就从前面的步骤在执行一遍,有相应的判断来跳转。
JDK1.7和JDK1.8的最大区别就就是:前者的第一次扩容是在数组长度大于10的时候,后者是在第一次add操作时。前者的第一次扩容是10到15,后者是0到10。
JDK1.8的ArrayList的注释没有修改,沿用JDK1.7的,所以呢,你提交了,你就是JDK的源码贡献者!!
点击图片阅读ArrayList.java源码
List实现类之二:LinkedList
- 对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高
新增方法:
- void addFirst(Object obj)
- void addLast(Object obj)
- Object getFirst()
- Object getLast()
- Object removeFirst()
- Object removeLast()
LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的first和last用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基 本结构。Node除了保存数据,还定义了两个变量:
- prev变量记录前一个元素的位置
- next变量记录下一个元素的位置
LinkedList源码分析
个人理解,有错误的请指教个人理解:JDK1.7和JDK1.8LinkedList源码一样,也不涉及到扩容,在次只做底层的源码分析。LinkedList为双向链表,顾名思义就是两个方向,这个方向分别指向前一个后一个元素的地址值。LinkedList不像ArrayList是Object[] 存储,而是以一种Node存储。Node在源码中封装为内部类,只供在内部使用。Node的核心结构分为三部分。Node(Node
prev, E element, Node next),我们添加的数据就是E element。当我们new LinkedList后。会有一个变量叫first它指向我们创建链条的头一个即第一个元素。另外一个变量叫last它指向我们创建链条的最后元素。正常的添加是在last后添加,查找的话就是从first往后查找。当我们添加元素时(即执行add操作)。调用add里面的linkLast方法并把需要添加的元素e作为参数放进去。linkLast方法中。把当前链表的最后一个元素即last给Node 变量名为l。如果第一次的话last就是null。下一步新建一个Node,新建的Node的prev指向last,element就是添加的元素值。next指向null。这时我们刚new的Node就成了整个链表的最后一个元素。接下来做一个判断。①判断l是否时null。如果为null则说明之前时没有添加过元素的。②否则也是就之前添加过元素。就把刚才新建的Node指向l.next。这个l是刚才没有添加元素前原链表的next属性。对于原链表的last元素的next属性原来是null的。现在new完新Node,新的Node的next就成了null。至此LinkedList的添加操作就完成了。对于插入和删除方式和添加操作类似。
小结:理解了LinkedList源码后,就很容易推出LinkedList的优点,就是插入和删除会更方便。插入的话只需要知道插入位置和插入位置下一个元素就可以,对于之前和之后的链表都不影响。不用像ArrayList每次插入删除频繁的前移或者后移。
点击图片可以跳转源码
- List 实现类之三:Vector
- Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
- 在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。
- 新增方法:
- void addElement(Object obj)
- void insertElementAt(Object obj,int index)
- void setElementAt(Object obj,int index)
- void removeElement(Object obj)
- void removeAllElements()
Vector源码分析
个人理解,有错误的请指教Vector特点:作为List接口的古老实现类;线程安全的,效率低。Vector和ArrayList源码相似,因为古老所以不建议使用了,一般都是用ArrayList,但是ArrayList线程不安,如果遇到线程安全的情况,就使用Collection class的synchronizedList返回一个线程安全的ArrayList;jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍。不作具体的分析了。
Collection子接口之二:Set接口
Set 接口概述
- Set接口是Collection的子接口,set接口没有提供额外的方法
- Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
- Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
Set实现类之一:HashSet
- HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
- HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除
性能。 - HashSet 具有以下特点:
- 不能保证元素的排列顺序
- HashSet 不是线程安全的
集合元素可以是 null
HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相 等,并且两个对象的 equals() 方法返回值也相等。
- 对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
重写 hashCode() 方法的基本原则
- 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
- 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。
对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
重写 equals() 方法的基本原则
以自定义的Customer类为例,何时需要重写equals()?- 当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不 同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法, 它们仅仅是两个对象。
- 因此,违反了“相等的对象必须具有相等的散列码”。
结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。
问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?
- 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
- 并且31只占用5bits,相乘造成数据溢出的概率较小。
- 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
- 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结 果只能被素数本身和被乘数还有1来整除!(减少冲突)
个人理解:HashSet的无序性不等于随机性。HashSet开始时创建的底层数组长度为16源码:DEFAULT_INITIAL_CAPACITY = 1 << 4。添加的过程并非按照数组的索引值存入,而是依据数据的哈希来存储。Set存储数据讲究无序性,存储的方式就是,每个数据生成一个哈希值,这个哈希值就决定了这个数据在数组中的存放位置。具体的存放位置也是通过某种散列函数或者称为某种算法来决定具体位置的。再进行add操作的时候也是类似的方法,算出哈希值,通过某种算法算出具体位置。这个时候就会出现两个个问题就是,①两个数据的哈希值可能相同,但是通过算法处理后的具体位置可能时相同的。这个时候就会判断这个位置上是否有元素。没有的话就直接放在这个位置,有的话这是就引入链表的结构了。JDK1.7和JDK1.8的区别是不同的,对于JDK1.7来说就是把后者相同具体存放位置的元素(即新元素)放在前者相同具体存放位置的元素的位置(旧元素),然后让新元素指向旧元素,。JDK1.8就是拿前者相同具体存放位置的元素指向后者相同具体存放位置的元素。(简记为:“七上八下”七是新的元素在上边,旧的在下边,八是新的元素在下边,旧的元素在上边)。挺难描述的,但是不难理解。②两个数据可能相同,经过某种算法算出具体存放位置相同。这个情况下开始时就会调用equals方法判断数据是否相同。相同的话就添加失败,不同的话就是上面的“七上八下”。好处就在于,添加的过程为保证不可重复性,就不同所有都遍历判断是否相同,只判断对应位置的数据是否相同,同就false,不同就True,添加成功。
个人理解可能还是有偏差的,可以查看尚硅谷宋红康老师的详细的讲解总结:
- Set实现类之二:LinkedHashSet
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入 顺序保存的。
- LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全 部元素时有很好的性能。
- LinkedHashSet 不允许集合元素重复。
个人理解:LinkedSet的有序是一种伪有序,存储的方式都是:数组+链表的结构。LinkedSet营造的有序简单理解就是固定的使双向链表的prev和next指向上一个add的数据和下一个add的数据。这就营造出了有序的状态
Set实现类之三:TreeSet
- TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
- TreeSet底层使用红黑树结构存储数据
- 新增的方法如下: (了解)
- Comparator comparator()
- Object first()
- Object last()
- Object lower(Object e)
- Object higher(Object e)
- SortedSet subSet(fromElement, toElement)
- SortedSet headSet(toElement)
- SortedSet tailSet(fromElement)
TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
排序—自然排序
- 自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元 素之间的大小关系,然后将集合元素按升序(默认情况)排列
- 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable接口。
- 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。
Comparable 的典型实现:
- BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小 进行比较
- Character:按字符的 unicode值来进行比较
- Boolean:true 对应的包装类实例大于 false 对应的包装类实例
- String:按字符串中字符的 unicode 值进行比较
- Date、Time:后边的时间、日期比前面的时间、日期大
向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添 加的所有元素都会调用compareTo()方法进行比较。
- 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同 一个类的对象。
- 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通 过 compareTo(Object obj) 方法比较返回值。
当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保 证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过 equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。 否则,让人难以理解。
排序—定制排序
- TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没 有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照 其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来 实现。需要重写compare(T o1,T o2)方法。
- 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
- 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构 造器。
- 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异 常。
- 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0
test01为自然排序,test02为定制排序
1 | import org.junit.Test; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
Map接口
Map接口概述
- Map与Collection并列存在。用于保存具有映射关系的数据:key-value
- Map 中的 key 和 value 都可以是任何引用类型的数据
- Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
- 常用String类作为Map的“键”
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
- Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和 Properties。其中,HashMap是 Map 接口使用频率最高的实现类
Map接口:常用方法
- 添加、删除、修改操作:
- Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
- void putAll(Map m):将m中的所有key-value对存放到当前map中
- Object remove(Object key):移除指定key的key-value对,并返回value
- void clear():清空当前map中的所有数据
- 元素查询的操作:
- Object get(Object key):获取指定key对应的value
- boolean containsKey(Object key):是否包含指定的key
- boolean containsValue(Object value):是否包含指定的value
- int size():返回map中key-value对的个数
- boolean isEmpty():判断当前map是否为空
- boolean equals(Object obj):判断当前map和参数对象obj是否相等
- 元视图操作的方法:
- Set keySet():返回所有key构成的Set集合
- Collection values():返回所有value构成的Collection集合
- Set entrySet():返回所有key-value对构成的Set集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98import org.testng.annotations.Test;
import java.util.*;
public class MapTest {
Map hashMap = new HashMap();
Map map = new HashMap();
public void AddRemoveModifyTest() {
// 添加
hashMap.put("A", 18);
hashMap.put("C", 15);
hashMap.put("B", 13);
// 修改
hashMap.put("A", 20);
System.out.println(hashMap);
map.put("a", 26);
map.put("e", 16);
map.put("d", 6);
hashMap.putAll(map);
System.out.println(hashMap);
// 移除
Object a = hashMap.remove("A");
System.out.println(a);
System.out.println(hashMap);
// 不存在返回null
System.out.println(hashMap.remove("D"));
// 清空
hashMap.clear();
System.out.println(hashMap);
System.out.println(hashMap.size());
}
public void InquireTest() {
hashMap.put("A", 18);
hashMap.put("C", 15);
hashMap.put("B", 13);
// 有则key.value
System.out.println(hashMap.get("A"));
// 没有则null
System.out.println(hashMap.get("D"));
// 是否有key,有则true,反之则false
boolean isExistKey = hashMap.containsKey("D");
System.out.println(isExistKey);
// 是否有value,有则true,反之则false
boolean isExistValue = hashMap.containsValue("13");
System.out.println(isExistValue);
// 是否是空
hashMap.clear();
System.out.println(hashMap.isEmpty());
}
public void MetaViewTest() {
hashMap.put("A", 18);
hashMap.put("C", 15);
hashMap.put("B", 13);
// 遍历key
Set set = hashMap.keySet();
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println();
// 遍历value
Collection values = hashMap.values();
for (Object o : values) {
System.out.println(o);
}
System.out.println();
// 遍历所有的key-value
Set entrySet = hashMap.entrySet();
Iterator iterator1 = entrySet.iterator();
for (int i = 0; i < entrySet.size(); i++) {
System.out.println(iterator1.next());
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20D:\PATH-EN\java-path\bin\java.exe ...
A
B
C
18
13
15
A=18
B=13
C=15
===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
- 添加、删除、修改操作:
Map实现类之一:HashMap
- HashMap是 Map 接口使用频率最高的实现类。
- 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
- 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
- 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
- 一个key-value构成一个entry
- 所有的entry构成的集合是Set:无序的、不可重复的
- HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
- HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
HashMap的存储结构
JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。HashMap源码中的重要常量
HashMap源码解析
- DEFAULT_INITIAL_CAPACITY : HashMap 的 默 认 容 量 ,16
- MAXIMUM_CAPACITY : HashMap 的 最 大 支 持 容 量 ,2^30
- DEFAULT_LOAD_FACTOR:HashMap的默认加载因子
- TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树
- UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表
- MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的 数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行 resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4 倍。)
- table:存储元素的数组,总是2的n次幂
- entrySet:存储具体元素的集
- size:HashMap中存储的键值对的数量
- modCount:HashMap扩容和结构改变的次数。
- threshold:扩容的临界值,=容量*填充因子
- loadFactor:填充因子
- HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时, 系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量 (Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个 bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
- 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引 用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。 而且新添加的元素作为链表的head。
添加元素的过程:
向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据 key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数 组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上 已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次 比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果 hash值不同,继续比较二者是否equals。如果返回值为true,则使用entry1的value 去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都 为false,则entry1仍可添加成功。entry1指向原有的entry元素。HashMap的扩容
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的 长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在 HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算 其在新数组中的位置,并放进去,这就是resize。那么HashMap什么时候进行扩容呢?
当HashMap 中的元素个数超过数组大小( 数组总大小length, 不是数组中个数 size) loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况 下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数 超过16 0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把 数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数, 那么预设元素的个数能够有效的提高HashMap的性能。HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个 HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系 时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表 中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为 “桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查 找bucket中的元素。
每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带 一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能 生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象 可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个 TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
那么HashMap什么时候进行扩容和树形化呢?
- 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数 size) loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的 默 认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认 情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中 元素个数超过16 0.75=12(这个值就是代码中的threshold值,也叫做临界值) 的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元 素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
- 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有 达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成 树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后, 下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
关于映射关系的key是否可以修改?answer:不要修改
映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算 每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关 系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。总结:JDK1.8相较于之前的变化:
- HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
- 当首次调用map.put()时,再创建长度为16的数组
- 数组为Node类型,在jdk7中称为Entry类型
- 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
- 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置 上的所有key-value对使用红黑树进行存储。
HashMap源码理解JDK1.7
个人理解,有错误的请指教HashMap实例化:new + 类的构造器。源码显示this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);这两个常量分别是16,和0.75f,前者是初始化的数组长度。后者叫默认加载因子,可以理解为如果数组填充的长度到达默认长度的3/4就进行扩容。查看带参的构造器,一个叫initialCapacity,一个叫loadFactor。上来①先判断initialCapacity是否小于零,如果你new的时候写的initialCapacity非法就会报 IllegalArgumentException 异常。②判断initialCapacity是否大于系统可以创建的最大值MAXIMUM_CAPACITY这个值叫做最大支持容量为(2^30)如果比这个数还大就给initialCapacity赋值为2^30.③判断loadFactor是否小于等于零,或者Float.isNaN(loadFactor),这个isNaN(loadFactor)方法的作用就是判断loadFactor是否是非数字的值(NAN),NAN 是Not a Number的缩写。是数字的话返回flase,不是数字则返回ture,这两个条件是或的关系,有一个不成立就报IllegalArgumentException异常。如果上面的①②③都不满足的话就给initialCapacity和loadFactor赋new的值。赋值成功后的下一步就有意思了,你new的HashMap(15, 0.75f),实际上是initialCapacity是16,原因就是,在这步会定义一个int capacity=1;人后让while(capacity < initialCapacity){capacity<<=1 }即只要capacity < initialCapacity,capacity就一直乘以2,15的话就是16,25的话就是32依次类似,所以 initialCapacity都是2的多少次幂,且最小为16。而在这个死循环中就直接刚才new的loadFactor赋值给this.loadFactor。下一步给threshold(临界值)赋值,依据就是在capacity*loadFactor和MAXIMUM_CAPACITY+1中取最小值,一般情况下都是前者。下一步就new Entry([capacity ])叫table的数组。HashMap的底层就是一个Entry< k,v >类型的数组。至此数组就造好了。
个人理解可能还是有偏差的,可以查看尚硅谷宋红康老师的详细的讲解总结:添加元素的过程:调用put(K key,V value)方法。查看put(K key,V value)源码。上来首先判断key是否是null,如果是的话return putForNullKey(value)此方法专门处理null的value。下一步进行计算key的哈希值,得到哈希值后,下一步就是确定put的元素在数组的位置,调用indexOf方法将key的哈希值和table的length作为参数传入。这个方法return h & (length-1);得到的就是在数组的位置。解释一下,h就是哈希值,length-1就是数组的长度减一。可以理解为某个随机的数与15取模得到的可能是0-15的任何数,只是这个方法中使用的是 & 即与符号,这种方式效率更高。在此举例,如果这个数据key1得到的哈希值值是15(二进制与计算同一为一,否则为0),15(1111)&15(1111)得0000十进制就是0,则这个数据存在table[ 0 ]位置,如果key2得哈希值为31即31(11111) & 15(1111)得1111十进制就是15,则这个数据存储在table[ 15 ]位置。本质与15取模一致,就是通过位运算符计算得话效率高。得到具体的存放位置后,下面进行for循环源码显示for (Entry< K,V > e = table[ i ];e !=null ;e = e.next),这段代码解释就是首先看索引位置时候否已经有元素,没有得话就可以直接赋值,把元素放在这个位置上源码显示:addEntry(hash,key,value,i)。有元素的话进入for循环,当然有元素的话可能也不只一个(因为存的是链表),for循环内部做的工作是①判断已经在这个位置上的元素的哈希值是否与新添加的元素的哈希值相同。②判断old的key与new的key是否相等。如果相等循环内部做一个覆盖的操作。也就是,用新的key的value替换old的key的value(此部分不好解释。但看源码的话可以理解)。下面解释添加元素的操作即addEntry(hash,key,value,i)。这个i就是要存放的位置。这个方法内部源码显示做判断,看size?>threshold(12)&&null !=table[i = bucketIndex],这段解释就是,看现在数组已有的元素的长度是否大于临界值了,和看这个元素的位置是否为空。在理解为:就是看这个位置是不是空,如果用有元素就会形成链表就需要扩容了。没有元素就放在这里。所以扩容的条件可以说是,数组的现有元素数量比临界值还大且这个位置已经有元素。满足扩容条件的话就会resize数组的长度,源码显示:resize(2*table.length)即是扩容为原来的2倍。前面的if判断主要就是判断是否扩容,如果不需要扩容。做createEntry(hash,key,value,bucketIndex)操作。对于这个方法,源码显示上来首先把原有位置上的元素取出定义为e,源码显示:Entry < K,V > e = table [ bucketIndex ]。新的元素放在数组的这个位置上,原来的元素作为新元素的next属性出现。源码显示:table[ bucketIndex ] =new Entry<>(hash,key,value,e);简单理解为:旧元素指向新元素的next。下一步进行size++。至此添加元素的操作完成。
HashMap源码理解JDK1.8
个人理解,有错误的请指教个人理解:如果理解了JDK1.8的ArrayList源码,HashMap的理解会更加透彻些,实例化的方式都是new+构造器。调用空参的构造器,上来只做一个操作:this.loadFactor = DEFAULT_LOAD_FACTOR;把加载因子赋值为默认的即为0.75f,加载因子的作用前面已经讲到了。我们发现在空参的构造器中并没有像1.7里面那样给一个默认的数组长度16。在jdk1.8中底层的元素不叫Entry了,叫做Node,本质基本没有区别。在1.8里面Node实现了Map.Entry。本质上还是Entry。至此实例化就完成了。
个人理解可能还是有偏差的,可以查看尚硅谷宋红康老师的详细的讲解总结:添加元素的过程:调用put(K key,V value)方法,这个方法return putVal(hash(key), key, value, false, true);hash(key)本质就是获取key的哈希值。后两者:onlyIfAbsent – if true, don’t change existing value;evict – if false, the table is in creation mode.putVal内部:①判断table是否是null,是的话进入,把table给resize。首次调用会进入。resize内部上来首先把table给oldTable,然后判断oldTable是否是null,是的话把0赋值给oldCap,把临界值给oldThr,临界值这里暂时是0。下一步定义两个变量,newCap,newThr都是0.下一步两个判断oldCap > 0和oldThr > 0,显然都不满足。执行else内部:newCap = DEFAULT_INITIAL_CAPACITY(注:把初始化的数组长度给newCap);newThr = (int)(DEFAULT_LOAD_FACTOR DEFAULT_INITIAL_CAPACITY)(注:求出临界值,即0.7516=12);下一步继续进行判断newThr == 0显然不是不进去。下一步threshold = newThr(注:给临界值赋值);下一步就是new一个 newTable=Node[ newCap ],newCap=16,把new的数组给table即table=newTable,返回newTable。至此数组的创建就完成了②判断put的元素在数组的位置。把这个位置的值赋值给p,看p是不是null,是的话就直接存。不是null的话就进入else,else其中的步骤比较复杂。这里大概说在else里面都做了什么。首先做的就是判断新添加的元素与原位置的元素的key的哈希值是否相等。当然原位置可能是一个元素也可能是一个链表。也可能是一个红黑树。所以新添加的元素就不能只和一个元素比,在这个位置的所有元素的哈希值都需要比较。这个时候如果第一次比完就发现新添加的元素的哈希值和旧元素的哈希值一样就做替换(这里的替换其实还没有替换,只是做了赋值。下一步才是替换)。把新的元素放在旧的位置上。反应到实际中就是如果你往数组put相同的key但是value不同的值的时候,后者会覆盖前者。下一步会做判断if (e != null),这一步才是真正的替换。前面说了和一个元素,和一个链表,一个红黑数比较吗(if(一个)…else if(红黑树)…else(链表))。所以每个内部的方式都不一样,都需要写不同的判断逻辑。这里有个特点,就是,如果链表的长度=8的时候就会把这个链表变为红黑树,也不是等于8就变红黑树,在这个里面还会判断,当前数组的长度的是否大于64,如果大于则转为红黑树,小于64的话去做扩容(resize),不做转为树。下面一步就是转为树的操作了。
点击图片可以跳转宋老师讲解
Map实现类之二:LinkedHashMap
- LinkedHashMap 是 HashMap 的子类
- 在HashMap存储结构的基础上,使用了一对双向链表来记录添加 元素的顺序
- 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代 顺序:迭代顺序与 Key-Value 对的插入顺序一致
Map接口
LinkedHashMap是HashMap的子类,看是来是有序的。通过某种手段展出出有序的状态,可以和,put的时候是调用父类的put方法。LinkedHashMap里面重写了newHashMap方法。这个方法内部new了一个LinkedHashMap.Entry,这里的Entry继承了HashMap.Node,继承的同时又多定义了两个变量,分别是before和after,记录前一个和后一个。所以就可以按照顺序来遍历。Set里面是一个个的元素,这一个一个元素相当于Map里面的Key。value是PRESENT源码显示是new Object().其实没有实际的意义。
- Map实现类之三:TreeMap
- TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。
- TreeMap 可以保证所有的 Key-Value 对处于有序状态。
- TreeSet底层使用红黑树结构存储数据
- TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有 的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口
- TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127import org.junit.Test;
import java.util.*;
public class TreeMapTest {
public void NaturalOrderTest() {
TreeMap treeMap = new TreeMap();
Person p1 = new Person("Jermyn", 18);
Person p2 = new Person("Jack", 20);
Person p3 = new Person("Jerry", 17);
Person p4 = new Person("Jackson", 23);
treeMap.put(p1, 90);
treeMap.put(p2, 85);
treeMap.put(p3, 85);
treeMap.put(p4, 95);
Set entrySet = treeMap.entrySet();
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
public void CustomSortingTest() {
TreeMap treeMap = new TreeMap(new Comparator() {
public int compare(Object o1, Object o2) {
if (o1 instanceof Person && o2 instanceof Person) {
Person p1 = (Person) o1;
Person p2 = (Person) o2;
return Integer.compare(p1.getAge(), p2.getAge());
}
throw new RuntimeException("输入类型不一致");
}
});
Person p1 = new Person("Jermyn", 18);
Person p2 = new Person("Jack", 20);
Person p3 = new Person("Jerry", 17);
Person p4 = new Person("Jackson", 23);
treeMap.put(p1, 90);
treeMap.put(p2, 85);
treeMap.put(p3, 85);
treeMap.put(p4, 95);
Set entrySet = treeMap.entrySet();
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
class Person implements Comparable {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = 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 equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
public int hashCode() {
return Objects.hash(name, age);
}
public String toString() {
return "Key:" +
"name='" + name + '\'' +
", age=" + age +
"\tValue:";
}
public int compareTo(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
int compare = this.name.compareTo(p.name);
if (compare != 0) {
return compare;
} else {
return Integer.compare(this.age, p.age);
}
} else {
throw new RuntimeException("输入类型不一致");
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11D:\PATH-EN\java-path\bin\java.exe ...
Key:name='Jerry', age=17 Value:=85
Key:name='Jermyn', age=18 Value:=90
Key:name='Jack', age=20 Value:=85
Key:name='Jackson', age=23 Value:=95
Key:name='Jack', age=20 Value:=85
Key:name='Jackson', age=23 Value:=95
Key:name='Jermyn', age=18 Value:=90
Key:name='Jerry', age=17 Value:=85
Process finished with exit code 0
- Map实现类之四:Hashtable
- Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,
- Hashtable是线程安全的。
- Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询 速度快,很多情况下可以互用。
- 与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
- 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
- Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
- Map实现类之五:Properties
- Properties 类是 Hashtable 的子类,该对象用于处理属性文件
- 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型
- 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class PropertiesTest {
public static void main(String[] args) throws Exception {
FileInputStream fis = null;
try {
// key和value都是String
Properties properties = new Properties();
fis = new FileInputStream("jdbc.properties");
properties.load(fis);
String name = properties.getProperty("name");
String password = properties.getProperty("password");
System.out.println("name = " + name + ", password = " + password);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3
4D:\PATH-EN\java-path\bin\java.exe ...
name = admin管理员, password = 123456789
Process finished with exit code 01
2name=admin管理员
password=123456789
Collections工具类
- Collections 是一个操作 Set、List 和 Map 等集合的工具类
- Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
排序操作:(均为static方法)
- reverse(List):反转 List 中元素的顺序
- shuffle(List):对 List 集合元素进行随机排序
- sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
- sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
- swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
查找、替换
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
- Object min(Collection)
- Object min(Collection,Comparator)
- int frequency(Collection,Object):返回指定集合中指定元素的出现次数
- void copy(List dest,List src):将src中的内容复制到dest中
- boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
Collections常用方法:同步控制
Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集 合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87import org.testng.annotations.Test;
import java.util.*;
public class CollectionsTest {
public void SortRelatedTest() {
ArrayList list = new ArrayList();
list.add(26);
list.add(12);
list.add(20);
list.add(70);
list.add(20);
list.add(12);
list.add(65);
list.add(54);
list.add(12);
System.out.println("original:" + list);
Collections.shuffle(list);
System.out.println("shuffle:" + list);
Collections.reverse(list);
System.out.println("reverse:" + list);
Collections.sort(list);
System.out.println("sort:" + list);
Collections.swap(list, 1, 3);
System.out.println("swap[index,index]:" + list);
}
public void FindModifyTest() {
ArrayList list = new ArrayList();
list.add(26);
list.add(12);
list.add(20);
list.add(21);
list.add(12);
System.out.println("original:" + list);
Comparable max = Collections.max(list);
System.out.println("maxOfList:" + max);
int frequency = Collections.frequency(list, 12);
System.out.println("frequencyOfElement(12)InList:" + frequency);
}
public void CopyTest() {
List list = new ArrayList();
list.add(26);
list.add(12);
list.add(20);
list.add(21);
list.add(12);
System.out.println("originalList:" + list);
List destination = Arrays.asList(new Object[list.size()]);
Collections.copy(destination, list);
System.out.println("CopyListAsDest:" + destination);
}
public void SyncTest() {
List list = new ArrayList();
list.add(26);
List list1 = Collections.synchronizedList(list);
System.out.println("经Collections.synchronizedList返回的list1是线程安全的");
HashMap hashMap = new HashMap();
hashMap.put("age", 18);
Map map = Collections.synchronizedMap(hashMap);
System.out.println("经Collections.synchronizedMap返回的map是线程安全的");
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22D:\PATH-EN\java-path\bin\java.exe ...
originalList:[26, 12, 20, 21, 12]
CopyListAsDest:[26, 12, 20, 21, 12]
original:[26, 12, 20, 21, 12]
maxOfList:26
frequencyOfElement(12)InList:2
original:[26, 12, 20, 70, 20, 12, 65, 54, 12]
shuffle:[65, 54, 20, 26, 12, 12, 70, 12, 20]
reverse:[20, 12, 70, 12, 12, 26, 20, 54, 65]
sort:[12, 12, 12, 20, 20, 26, 54, 65, 70]
swap[index,index]:[12, 20, 12, 12, 20, 26, 54, 65, 70]
经Collections.synchronizedList返回的list1是线程安全的
经Collections.synchronizedMap返回的map是线程安全的
===============================================
Default Suite
Total tests run: 4, Passes: 4, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
第五章 泛型
为什么要有泛型
- 泛型的设计背景
集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的 对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来 解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于 这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个 参数,这个类型参数叫做泛型。Collection,List ,ArrayList 这个 就 是类型参数,即泛型。 - 泛型的概念
- 所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类 型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如, 继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实 际的类型参数,也称为类型实参)。
- 从JDK1.5以后,Java引入了“参数化类型(Parameterized type)”的概念, 允许我们在创建集合时再指定集合元素的类型,正如:List
,这表明 该List只能保存字符串类型的对象。 - JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持, 从而可以在声明集合变量、创建集合对象时传入类型实参。
在集合中使用泛型
1 | import org.testng.annotations.Test; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
自定义泛型结构
泛型的声明
interface List和 class GenTest :其中,T,K,V不代表值,而是表示类型。这里使用任意字母都可以。常用T表示,是Type的缩写。 泛型的实例化:
- 一定要在类名后面指定类型参数的值(类型)。如:
- List
strList = new ArrayList (); - Iterator
iterator = customers.iterator();
- List
- T只能是类,不能用基本数据类型填充。但可以使用包装类填充
- 把一个集合中的内容限制为一个特定的数据类型,这就是generics背后的核心思想
- 一定要在类名后面指定类型参数的值(类型)。如:
自定义泛型结构:泛型类、泛型接口
- 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:
- 泛型类的构造器如下:public GenericClass(){}。 错误的:public GenericClass
(){} - 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
- 泛型不同的引用不能相互赋值。>尽管在编译时ArrayList
和ArrayList 是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。 - 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价 于Object。经验:泛型要使用一路都用。要不用,一路都不要用。
- 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
- jdk1.7,泛型的简化操作:ArrayList
flist = new ArrayList<>(); - 泛型的指定中不能使用基本数据类型,可以使用包装类替换。
- 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
- 异常类不能是泛型的
- 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
子类不保留父类的泛型:按需实现
- 没有类型 擦除
- 具体类型
子类保留父类的泛型:泛型子类
- 全部保留
- 部分保留
结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型
- 自定义泛型结构:泛型方法
- 方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型 方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
泛型方法的格式: - 格式
1
2
3[访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 抛出的异常
eg:
public <E> List copyFromArrayToList(E[] arr) throws RuntimeException示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import java.util.ArrayList;
import java.util.List;
public class GenericMethodTest {
public static void main(String[] args) {
// 泛型类的类型跟泛型方法的类型无关
Generic<String> stringGeneric = new Generic<>();
Integer[] intArr = {1, 6, 5, 4, 9, 8};
List<Integer> list = stringGeneric.copyFromArrayToList(intArr);
System.out.println(list);
}
}
class Generic<E> {
public <E> List copyFromArrayToList(E[] arr) throws RuntimeException{
ArrayList<E> list = new ArrayList<>();
for (E e : arr) {
list.add(e);
}
return list;
}
}点击查看运行结果
1
2
3
4D:\PATH-EN\java-path\bin\java.exe ...
[1, 6, 5, 4, 9, 8]
Process finished with exit code 0
- 方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型 方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
通配符的使用
- 使用类型通配符:?
- 比如:List<?>,Map<?,?>
- List<?>是List< String >、List< Object >等各种泛型List的父类。
- 读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型 是什么,它包含的都是Object。
- 写入list中的元素时,不行。因为我们不知道c的元素类型,我们不能向其中添加对象。
- 唯一的例外是null,它是所有类型的成员。
- 因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集 合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。
- 唯一的例外的是null,它是所有类型的成员。
另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object。
1
2Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时错误示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class WildcardTest {
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
list1.add(3);
List<String> list2 = new LinkedList<>();
list2.add("Jer");
list2.add("Jac");
list2.add("Jerry");
// 泛型不一样,不可赋值
// list1=list2;
// ? 指的是list1和list2的公共的父类
List<?> list = null;
list = list2;
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
list = list1;
Iterator<?> iterator1 = list.iterator();
while (iterator1.hasNext()) {
Object next = iterator1.next();
System.out.println(next);
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9D:\PATH-EN\java-path\bin\java.exe ...
Jer
Jac
Jerry
1
2
3
Process finished with exit code 0通配符的使用:有限制的通配符
- <?>
- 允许所有泛型的引用调用
- 通配符指定上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
- 通配符指定下限super:使用时指定的类型不能小于操作的类,即>=
- 举例:
- <? extends Number>(无穷小 , Number]只允许泛型为Number及Number子类的引用调用
- <? super Number> [Number , 无穷大)只允许泛型为Number及Number父类的引用调用
- <? extends Comparable>只允许泛型为实现Comparable接口的实现类的引用调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import java.util.List;
public class GenericTest {
public static void main(String[] args) {
List<? extends Person> list1 = null;
List<? super Person> list2 = null;
List<Person> list3 = null;
List<Student> list4 = null;
List<Object> list5 = null;
list1 = list3;
list1 = list4;
// 此处不可以,extends可以理解为 <= Person 的类或者子类
// list1 = list5;
list2 = list3;
// 此处不可以,super可以理解为 >= Person 的类或者父类
// list2 = list4;
list2 = list5;
}
}
class Person {
}
class Student extends Person {
}
第六章 IO流
File类的使用
- java.io.File类:文件和文件目录路径的抽象表示形式,与平台无关
- File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。
- 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对 象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。
File对象可以作为参数传递给流的构造器
点击图片可以跳转源码- File 类的使用:常用构造器
- public File(String pathname)—>以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。
- 绝对路径:是一个固定的路径,从盘符开始
相对路径:是相对于某个位置开始
public File(String parent,String child)—>以parent为父路径,child为子路径创建File对象。
- public File(File parent,String child)—>根据一个父File对象和子文件路径创建File对象
File 类的使用:路径分隔符
- 路径中的每级目录之间用一个路径分隔符隔开。
路径分隔符和系统有关:
- windows和DOS系统默认使用“\”来表示
- UNIX和URL使用“/”来表示
Java程序支持跨平台运行,因此路径分隔符要慎用。
- 为了解决这个隐患,File类提供了一个常量:public static final String separator。根据操作系统,动态的提供分隔符。
- 举例:
1
2
3File file1 = new File("d:\\jermyn\\info.txt");
File file2 = new File("d:" + File.separator + "jermyn" + File.separator + "demo.txt");
File file3 = new File("d:/jermyn");
File 类的使用:常用方法
File类的获取功能
- public String getAbsolutePath():获取绝对路径
- public String getPath() :获取路径
- public String getName() :获取名称
- public String getParent():获取上层文件目录路径。若无,返回null
- public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
public long lastModified() :获取最后一次的修改时间,毫秒值
public String[] list() :获取指定目录下的所有文件或者文件目录的名称数组
- public File[] listFiles() :获取指定目录下的所有文件或者文件目录的File数组
File类的重命名功能
- public boolean renameTo(File dest):把文件重命名为指定的文件路径File类的判断功能
- public boolean isDirectory():判断是否是文件目录
- public boolean isFile() :判断是否是文件
- public boolean exists() :判断是否存在
- public boolean canRead() :判断是否可读
- public boolean canWrite() :判断是否可写
- public boolean isHidden() :判断是否隐藏
File类的创建功能
- public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false
- public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。 如果此文件目录的上层目录不存在,也不创建。
- public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建
注意事项:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目 路径下。
File类的删除功能
public boolean delete():删除文件或者文件夹
删除注意事项:
- Java中的删除不走回收站。
- 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录
1 | import org.testng.annotations.Test; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
IO流原理及流的分类
Java IO原理
- I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。
- Java程序中,对于数据的输入/输出操作以“流(stream)” 的方式进行。
- java.io包下提供了各种“流”类和接口,用以获取不同种类的 数据,并通过标准的方法输入或输出数据。
- 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
- 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。
流的分类
- 按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)
- 按数据流的流向不同分为:输入流,输出流
- 按流的角色的不同分为:节点流,处理流
抽象基类 节点流(文件流) 缓冲流 InputStream FileInputStream (read(byte[] buffer)) BufferedInputStream (read(byte[] buffer)) OutputStream FileOutputStream (write(byte[] buffer,0,len) BufferedOutputStream (write(byte[] buffer,0,len) / flush() Reader FileReader (read(char[] cbuf)) BufferedReader (read(char[] cbuf) / readLine()) Writer FileWriter (write(char[] cbuf,0,len) BufferedWriter (write(char[] cbuf,0,len) / flush()
InputStream & Reader
- InputStream 和 Reader 是所有输入流的基类。
InputStream(典型实现:FileInputStream)
- int read()
- int read(byte[] b)
- int read(byte[] b, int off, int len)
Reader(典型实现:FileReader)
- int read()
- int read(char [] c)
- int read(char [] c, int off, int len)
程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资 源,所以应该显式关闭文件 IO 资源。
- FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader
InputStream
- int read():从输入流中读取数据的下一个字节。返回 0 到 255 范围内的int 字节值。如果因 为已经到达流末尾而没有可用的字节,则返回值-1。
- int read(byte[] b):从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。如果因为已 经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取 的字节数。
- int read(byte[] b, int off,int len):将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个字节,但读取
的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于 文件末尾而没有可用的字节,则返回值-1。 - public void close() throws IOException“关闭此输入流并释放与该流关联的所有系统资源。
Reader
- int read():读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个
- 字节的Unicode码),如果已到达流的末尾,则返回 -1
- int read(char[] cbuf):将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
- int read(char[] cbuf,int off,int len):将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字 符。如果已到达流的末尾,则返回-1。否则返回本次读取的字符数。
- public void close() throws IOException:关闭此输入流并释放与该流关联的所有系统资源。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84import org.testng.annotations.Test;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class FileReaderTest {
public void testFileReader() {
FileReader fr = null;
try {
//1.实例化File类的对象,指明要操作的文件
File fileR = new File("C:\\Users\\Administrator\\Desktop\\IO\\demo.txt");
//2.提供具体的流
fr = new FileReader(fileR);
//3.数据的读入
int date;
while ((date = fr.read()) != -1) {
System.out.print((char) date);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fr != null) {
//4.流的关闭操作
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void testFileReader01() {
FileReader fr = null;
try {
//1.实例化File类的对象,指明要操作的文件
File fileR = new File("C:\\Users\\Administrator\\Desktop\\IO\\demo.txt");
//2.提供具体的流
fr = new FileReader(fileR);
//3.读入操作
char[] charBuffer = new char[1024];
int len;
while ((len = fr.read(charBuffer)) != -1) {
// 方式一:
System.out.println();
System.out.println("----------------方式一----------------");
for (int i = 0; i <= len; i++) {
System.out.print(charBuffer[i]);
}
System.out.println();
System.out.println("----------------方式二----------------");
// 方式二:
String str = new String(charBuffer, 0, len);
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null) {
try {
//4.流的关闭操作
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14D:\PATH-EN\java-path\bin\java.exe ...
abcdefghiklmnopqrstuvwxyz
----------------方式一----------------
abcdefghiklmnopqrstuvwxyz
----------------方式二----------------
abcdefghiklmnopqrstuvwxyz
===============================================
Default Suite
Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
OutputStream & Writer
OutputStream 和 Writer 也非常相似:
- void write(int b/int c);
- void write(byte[] b/char[] cbuf);
- void write(byte[] b/char[] buff, int off, int len);
- void flush();
- void close(); 需要先刷新,再关闭此流
因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组, 即以 String 对象作为参数
- void write(String str);
- void write(String str, int off, int len);
FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputStream 用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter
OutputStream
- void write(int b):将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写 入的字节是参数b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。
- void write(byte[] b):将b.length 个字节从指定的byte 数组写入此输出流。write(b) 的常规协定是:应该 与调用write(b, 0, b.length) 的效果完全相同。
- void write(byte[] b,int off,int len):将指定byte 数组中从偏移量 off 开始的len 个字节写入此输出流。
- public void flush()throws IOException:刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立
即写入它们预期的目标。 - public void close() throws IOException:关闭此输出流并释放与该流关联的所有系统资源。
Writer
- void write(int c):写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即
写入0 到 65535 之间的Unicode码。 - void write(char[] cbuf):写入字符数组。
- void write(char[] cbuf,int off,int len):写入字符数组的某一部分。从off开始,写入len个字符
- void write(String str):写入字符串。
- void write(String str,int off,int len):写入字符串的某一部分。
- void flush():刷新该流的缓冲,则立即将它们写入预期目标。
- public void close() throws IOException:关闭此输出流并释放与该流关联的所有系统资源。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83import org.testng.annotations.Test;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterTest {
public void testFileWriter() {
FileWriter fw = null;
try {
//1.提供File类的对象,指明写出到的文件
File fileW = new File("C:\\Users\\Administrator\\Desktop\\IO\\dest.txt");
//2.提供FileWriter的对象,用于数据的写出
// append:true 理解为追加,false理解为覆盖
fw = new FileWriter(fileW, true);
//3.写出的操作
fw.write("I have a dream!\n");
fw.write("you need to have a dream!");
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.流资源的关闭
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public void testFileReaderWriter() {
FileReader fr = null;
FileWriter fw = null;
try {
//1.提供File类的对象,指明读入的文件
File fileR = new File("C:\\Users\\Administrator\\Desktop\\IO\\this.txt");
//1.1提供File类的对象,指明写出到的文件
File fileW = new File("C:\\Users\\Administrator\\Desktop\\IO\\dest.txt");
//2.创建输入流和输出流的对象
fr = new FileReader(fileR);
fw = new FileWriter(fileW);
//3.数据的读入和写出操作
char[] charsBuffer = new char[5];
int len;
while ((len = fr.read(charsBuffer)) != -1) {
//每次写出len个字符
fw.write(charsBuffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭流资源
try {
if (fr != null) {
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9D:\PATH-EN\java-path\bin\java.exe ...
===============================================
Default Suite
Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
- void write(int c):写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即
节点流(或文件流)
节点流(或文件流):注意点
- 定义文件路径时,注意:可以用“/”或者“\”。
- 在写入一个文件时,如果使用构造器FileOutputStream(file),则目录下有同名文件将被覆盖。
- 如果使用构造器FileOutputStream(file,true),则目录下的同名文件不会被覆盖, 在文件内容末尾追加内容。
- 在读取文件时,必须保证该文件已存在,否则报异常。
- 字节流操作字节,比如:.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt
- 字符流操作字符,只能操作普通文本文件。最常见的文本文件:.txt,.java,.c,.cpp 等语言的源代码。尤其注意.doc,excel,ppt这些不是文 本文件。
步骤
- 处理对象
- 创建流
- 具体处理流程
- 关闭流
示例,创建处理文件的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
copyFile("C:\\Users\\Administrator\\Desktop\\this.mp4",
"C:\\Users\\Administrator\\Desktop\\dest.mp4");
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start) + "ms");
}
public static void copyFile(String thisPath, String destPath) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
File fileR = new File(thisPath);
File fileW = new File(destPath);
fis = new FileInputStream(fileR);
fos = new FileOutputStream(fileW);
int len;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
System.out.println("复制成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3
4
5D:\PATH-EN\java-path\bin\java.exe ...
复制成功
花费的时间为:42ms
Process finished with exit code 0注:处理文本文件,例如txt,.java,.py等的使用FileReader,FileWriter,处理字节流的如.mp3,mp4等的,使用FileOutputStream,FileInputStream,处理文件。文本文件也可以用字节流处理,但是字节流不可以用文本处理方式。
缓冲流
为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类 时,会创建一个内部缓冲区数组,缺省使用8192个字节(8Kb)的缓冲区。
缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:
- BufferedInputStream 和 BufferedOutputStream
- BufferedReader 和 BufferedWriter
当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区
- 当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从 文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中 读取下一个8192个字节数组。
- 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满, BufferedOutputStream才会把缓冲区中的数据一次性- 写到文件里。使用方法 flush()可以强制将缓冲区的内容全部写入输出流
- 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也 会相应关闭内层节点流
- flush()方法的使用:手动将buffer中内容写入文件
- 如果是带缓冲区的流对象的close()方法,不但会关闭流,还会在关闭流之前刷 新缓冲区,关闭后不能再写出
示例:BufferedReader和BufferedWriter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43import java.io.*;
public class BufferedReaderBufferedWriterTest {
public static void main(String[] args) {
BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(
new FileReader(
new File("C:\\Users\\Administrator\\Desktop\\this.txt")));
bw = new BufferedWriter(
new FileWriter(
new File("C:\\Users\\Administrator\\Desktop\\dest.txt")));
String date;
while ((date = br.readLine()) != null) {
bw.write(date);
bw.newLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3D:\PATH-EN\java-path\bin\java.exe ...
Process finished with exit code 0练习:获取文本上每个字符出现的次数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class WordCountTest {
public static void main(String[] args) {
testWordCount();
}
public static void testWordCount() {
FileReader fr = null;
BufferedWriter bw = null;
try {
//1.创建Map集合
Map<Character, Integer> map = new HashMap<Character, Integer>();
//2.遍历每一个字符,每一个字符出现的次数放到map中
fr = new FileReader("C:\\Users\\Administrator\\Desktop\\this.txt");
int c = 0;
while ((c = fr.read()) != -1) {
//int 还原 char
char ch = (char) c;
// 判断char是否在map中第一次出现
if (map.get(ch) == null) {
map.put(ch, 1);
} else {
map.put(ch, map.get(ch) + 1);
}
}
//3.把map中数据存在文件count.txt
//3.1 创建Writer
bw = new BufferedWriter(new FileWriter("C:\\Users\\Administrator\\Desktop\\wordcount.txt"));
//3.2 遍历map,再写入数据
Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
for (Map.Entry<Character, Integer> entry : entrySet) {
switch (entry.getKey()) {
case ' ':
bw.write("空格=" + entry.getValue());
break;
case '\t'://\t表示tab 键字符
bw.write("tab键=" + entry.getValue());
break;
case '\r'://
bw.write("回车=" + entry.getValue());
break;
case '\n'://
bw.write("换行=" + entry.getValue());
break;
default:
bw.write(entry.getKey() + "=" + entry.getValue());
break;
}
bw.newLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关流
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3D:\PATH-EN\java-path\bin\java.exe ...
Process finished with exit code 0
转换流
- 转换流提供了在字节流和字符流之间的转换
- Java API提供了两个转换流:
- InputStreamReader:将InputStream转换为Reader
- OutputStreamWriter:将Writer转换为OutputStream
- 字节流中的数据都是字符时,转成字符流操作更高效。
- 很多时候我们使用转换流来处理文件乱码问题。实现编码和 解码的功能。
InputStreamReader
- 实现将字节的输入流按指定字符集转换为字符的输入流。
- 需要和InputStream“套接”。
- 构造器
- public InputStreamReader(InputStream in)
- public InputSreamReader(InputStream in,String charsetName)
- 如: Reader isr = new InputStreamReader(System.in,”gbk”);
OutputStreamWriter
- 实现将字符的输出流按指定字符集转换为字节的输出流。
- 需要和OutputStream“套接”。
- 构造器
- public OutputStreamWriter(OutputStream out)
- public OutputSreamWriter(OutputStream out,String charsetName)
示例:UTF-8转GBK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51import java.io.*;
public class OutputStreamWriterTest {
public static void main(String[] args) throws IOException {
testOutputStreamWriter();
}
public static void testOutputStreamWriter() {
InputStreamReader isrU = null;
OutputStreamWriter oswG = null;
try {
FileInputStream fisU = new FileInputStream(
"C:\\Users\\Administrator\\Desktop\\this.txt");
FileOutputStream fosG = new FileOutputStream(
"C:\\Users\\Administrator\\Desktop\\dest.txt");
isrU = new InputStreamReader(fisU, "UTF-8");
oswG = new OutputStreamWriter(fosG, "GBK");
char[] chars = new char[1024];
int len;
while ((len = isrU.read(chars)) != -1) {
oswG.write(chars, 0, len);
}
System.out.println("操作成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (isrU != null){
try {
isrU.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (oswG != null){
try {
oswG.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3
4D:\PATH-EN\java-path\bin\java.exe ...
操作成功
Process finished with exit code 0
字符编码
编码表的由来
- 计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识 别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。 这就是编码表。
常见的编码表
- ASCII:美国标准信息交换码。用一个字节的7位可以表示。
- ISO8859-1:拉丁码表。欧洲码表。用一个字节的8位表示。
- GB2312:中国的中文编码表。最多两个字节编码所有字符
- GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
- Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的 字符码。所有的文字都用两个字节来表示。
- UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
Unicode不完美,这里就有三个问题,一个是,我们已经知道,英文字母只用 一个字节表示就够了,第二个问题是如何才能区别Unicode和ASCII?计算机 怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果 和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节, 就少了很多值无法用于表示字符,不够表示所有字符。Unicode在很长一段时 间内无法推广,直到互联网的出现。
- 面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF- 8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的 编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
- Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯 一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的 Unicode编码是UTF-8和UTF-16。
标准输入、输出流
- System.in和System.out分别代表了系统标准的输入和输出设备
- 默认输入设备是:键盘,输出设备是:显示器
- System.in的类型是InputStream
- System.out的类型是PrintStream,其是OutputStream的子类 FilterOutputStream 的子类
- 重定向:通过System类的setIn,setOut方法对默认设备进行改变。
- public static void setIn(InputStream in)
- public static void setOut(PrintStream out)
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class SystemInOutTest {
public static void main(String[] args) {
BufferedReader br = null;
try {
InputStreamReader isr = new InputStreamReader(System.in);
br = new BufferedReader(isr);
while (true) {
System.out.println("请输入字符串:");
String data = br.readLine();
if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
System.out.println("程序结束");
break;
}
String upperCase = data.toUpperCase();
System.out.println(upperCase);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12D:\PATH-EN\java-path\bin\java.exe ...
请输入字符串:
123
123
请输入字符串:
jermyn
JERMYN
请输入字符串:
e
程序结束
Process finished with exit code 0
打印流
- 实现将基本数据类型的数据格式转化为字符串输出
- 打印流:PrintStream和PrintWriter
- 提供了一系列重载的print()和println()方法,用于多种数据类型的输出
- PrintStream和PrintWriter的输出不会抛出IOException异常
PrintStream和PrintWriter有自动flush功能 - PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。
- 在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。
- System.out返回的是PrintStream的实例
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
public class PrintStreamWriter {
public static void main(String[] args) {
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(new File("C:\\Users\\Administrator\\Desktop\\this.txt"));
// 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
ps = new PrintStream(fos, true);
if (ps != null) {// 把标准输出流(控制台输出)改成文件
System.setOut(ps);
}
for (int i = 0; i <= 255; i++) { // 输出ASCII字符
System.out.print((char) i);
if (i % 50 == 0) { // 每50个数据一行
System.out.println(); // 换行
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ps != null) {
ps.close();
}
}
}
}点击查看运行结果
1
2
3D:\PATH-EN\java-path\bin\java.exe ...
Process finished with exit code 0
数据流
- 为了方便地操作Java语言的基本数据类型和String的数据,可以使用数据流。
- 数据流有两个类:(用于读取和写出基本数据类型、String类的数据)
- DataInputStream 和 DataOutputStream
- 分别“套接”在 InputStream 和 OutputStream 子类的流上
DataInputStream中的方法
- boolean readBoolean()
- char readChar()
- double readDouble()
- long readLong()
- String readUTF()
- byte readByte()
- float readFloat()
- short readShort()
- int readInt()
- void readFully(byte[] b)
DataOutputStream中的方法
- 将上述的方法的read改为相应的write即可。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48import java.io.*;
public class DateStreamTest {
public static void main(String[] args) {
DataOutputStream dos = null;
DataInputStream dis = null;
try {
dos = new DataOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\data.txt"));
dis = new DataInputStream(new FileInputStream("C:\\Users\\Administrator\\Desktop\\data.txt"));
dos.writeUTF("Jermyn");
dos.flush();
dos.writeInt(23);
dos.flush();
dos.writeBoolean(true);
dos.flush();
String name = dis.readUTF();
int age = dis.readInt();
boolean isMale = dis.readBoolean();
System.out.println("name = " + name);
System.out.println("age = " + age);
System.out.println("isMale = " + isMale);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (dos != null) {
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (dis != null) {
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3
4
5
6D:\PATH-EN\java-path\bin\java.exe ...
name = Jermyn
age = 23
isMale = true
Process finished with exit code 0
- 将上述的方法的read改为相应的write即可。
对象流
ObjectInputStream和OjbectOutputSteam
- 用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可 以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
序列化:用ObjectOutputStream类保存基本类型数据或对象的机制
- 反序列化:用ObjectInputStream类读取基本类型数据或对象的机制
- ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量
对象的序列化
- 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从 而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传 输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原 来的Java对象
- 序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据, 使其在保存和传输时可被还原
- 序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返 回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础
- 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可 序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。 否则,会抛出NotSerializableException异常
- Serializable
- Externalizable
凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:
- private static final long serialVersionUID;
- serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象 进行版本控制,有关各版本反序列化时是否兼容。
- 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自 动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。故建议,显式声明。
简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验 证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的 serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同 就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异 常。(InvalidCastException)
使用对象流序列化对象
- 若某个类实现了 Serializable 接口,该类的对象就是可序列化的:
- 创建一个 ObjectOutputStream
- 调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象
- 注意写出一次,操作flush()一次
反序列化
- 创建一个 ObjectInputStream
- 调用 readObject() 方法读取流中的对象
强调:如果某个类的属性不是基本数据类型或 String 类型,而是另一个 引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的 Field 的类也不能序列化
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87import java.io.*;
public class ObjectInputOutputStreamTest {
public static void main(String[] args) {
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
// 序列化(Serialization)
File file = new File("C:\\Users\\Administrator\\Desktop\\file.dat");
FileOutputStream fos = new FileOutputStream(file);
oos = new ObjectOutputStream(fos);
// ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Administrator\\Desktop\\file.dat"));
oos.writeObject(new Person("Jermyn", 18));
oos.flush();
// 反序列化(Deserialization)
ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Administrator\\Desktop\\file.dat"));
Object o = ois.readObject();
Person p = (Person) o;
System.out.println(p);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
class Person implements Serializable{
private static final long serialVersionUID = 475463534532L;
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = 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;
}
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}点击查看运行结果
1
2
3
4D:\PATH-EN\java-path\bin\java.exe ...
Person{name='Jermyn', age=18}
Process finished with exit code 0
随机存取文件流
RandomAccessFile 类
- RandomAccessFile 声明在java.io包下,但直接继承于java.lang.Object类。并 且它实现了DataInput、DataOutput这两个接口,也就意味着这个类既可以读也 可以写。
RandomAccessFile 类支持 “随机访问” 的方式,程序可以直接跳到文件的任意
地方来读、写文件- 支持只访问文件的部分内容
- 可以向已存在的文件后追加内容
RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile 类对象可以自由移动记录指针:
- long getFilePointer():获取文件记录指针的当前位置
- void seek(long pos):将文件记录指针定位到 pos 位置
RandomAccessFile 类
构造器
- public RandomAccessFile(File file, String mode)
- public RandomAccessFile(String name, String mode)
创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指 定 RandomAccessFile 的访问模式:
如果模式为只读r。则不会创建文件,而是会去读取一个已经存在的文件, 如果读取的文件不存在则会出现异常。 如果模式为rw读写。如果文件不 存在则会去创建文件,如果存在则不会创建。
关于RandomAccessFile的三种使用方法
常规方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileTest {
public static void main(String[] args) {
RandomAccessFile rafR = null;
RandomAccessFile rafW = null;
try {
rafR = new RandomAccessFile(
new File("C:\\Users\\Administrator\\Desktop\\this.mp4"), "r");
rafW = new RandomAccessFile(
new File("C:\\Users\\Administrator\\Desktop\\dest.mp4"), "rw");
byte[] bytes = new byte[1024];
int len;
while ((len = rafR.read(bytes)) != -1) {
rafW.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (rafR != null){
try {
rafW.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (rafW != null){
try {
rafR.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3D:\PATH-EN\java-path\bin\java.exe ...
Process finished with exit code 0覆盖文件的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileCoverTest {
public static void main(String[] args) {
RandomAccessFile rafRW = null;
try {
rafRW = new RandomAccessFile(
new File("C:\\Users\\Administrator\\Desktop\\this.txt"), "rw");
rafRW.write("abcdefghijklmnopqrstuvwxyz".getBytes());
rafRW.seek(2);
rafRW.write("CDEF".getBytes());
rafRW.seek(rafRW.length());
rafRW.write("123456789".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (rafRW != null) {
try {
rafRW.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3D:\PATH-EN\java-path\bin\java.exe ...
Process finished with exit code 0实现插入操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* Package: cn.jermyn.test02
* Description:seek位置插入数据dataD思路:(讲究一个字拼)
记录seek到File.length的数据dataB,在seek位置插入需要插入的数据dataD,在dataD后面插入dataB
* Author: Jermyn
* Version: 0.0.1
*/
public class RandomAccessFileInsertTest {
public static void main(String[] args) {
RandomAccessFile rafRW = null;
try {
File file = new File("C:\\Users\\Administrator\\Desktop\\this.txt");
rafRW = new RandomAccessFile(file, "rw");
rafRW.write("abcdefghijklmnopqrstuvwxyz".getBytes());
rafRW.seek(2);
StringBuilder builder = new StringBuilder((int) file.length());
byte[] bytes = new byte[1024];
int len;
while ((len = rafRW.read(bytes)) != -1) {
builder.append(new String(bytes), 0, len);
}
rafRW.seek(2);
rafRW.write("CEDF".getBytes());
rafRW.write(builder.toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (rafRW != null) {
try {
rafRW.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
1
2
3D:\PATH-EN\java-path\bin\java.exe ...
Process finished with exit code 0
第七章 网络编程
通信要素1:IP和端口号
IP 地址:InetAddress
- 唯一的标识 Internet 上的计算机(通信实体)
- 本地回环地址(hostAddress):127.0.0.1 主机名(hostName):localhost
- IP地址分类方式1:IPV4 和 IPV6
- IPV4:4个字节组成,4个0-255。大概42亿,30亿都在北美,亚洲4亿。2011年初已经用尽。以点分十进制表示,如192.168.0.1
- IPV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示, 数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984
- IP地址分类方式2:公网地址(万维网使用)和私有地址(局域网使用)。192.168. 开头的就是私有址址,范围即为192.168.0.0—192.168.255.255,专门为组织机 构内部使用
- 特点:不易记忆
端口号标识正在计算机上运行的进程(程序)
- 不同的进程有不同的端口号
- 被规定为一个 16 位的整数 0~65535。
端口分类:
- 公认端口:0~1023。被预先定义的服务通信占用(如:HTTP占用端口80,FTP占用端口21,Telnet占用端口23)
- 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占用端口8080,MySQL占用端口3306,Oracle占用端口1521等)。
- 动态/私有端口:49152~65535。
端口号与IP地址的组合得出一个网络套接字:Socket。
InetAddress类
Internet上的主机有两种方式表示地址:
- 域名(hostName):www.jermyn.cn
- IP 地址(hostAddress):76.76.21.123
InetAddress类主要表示IP地址,两个子类:Inet4Address、Inet6Address。
- InetAddress 类对象含有一个Internet主机地址的域名和 IP 地址
- 域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。 ———-域名解析
InetAddress类没有提供公共的构造器,而是提供了如下几个静态方法来获取InetAddress实例
- public static InetAddress getLocalHost()
- public static InetAddress getByName(String host)
InetAddress提供了如下几个常用的方法
- public String getHostAddress():返回 IP 地址字符串(以文本表现形式)。
- public String getHostName():获取此 IP 地址的主机名
- public boolean isReachable(int timeout):测试是否可以达到该地址
通信要素2:网络协议
TCP/IP协议簇
传输层协议中有两个非常重要的协议:
- 传输控制协议TCP(Transmission Control Protocol)
- 用户数据报协议UDP(User Datagram Protocol)。
TCP/IP 以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。
- IP(Internet Protocol)协议是网络层的主要协议,支持网间互连的数据通信。
- TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即物理链路层、IP层、传输层和应用层。
TCP 和 UDP
TCP协议:
- 使用TCP协议前,须先建立TCP连接,形成传输数据通道
- 传输前,采用“三次握手”方式,点对点通信,是可靠的
- TCP协议进行通信的两个应用进程:客户端、服务端。
- 在连接中可进行大数据量的传输
- 传输完毕,需释放已建立的连接,效率低
UDP协议:
- 将数据、源、目的封装成数据包,不需要建立连接
- 每个数据报的大小限制在64K内
- 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
- 可以广播发送
- 发送数据结束时无需释放资源,开销小,速度快
Socket
- 利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实 上的标准。
- 网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标 识符套接字。
- 通信的两端都要有Socket,是两台机器间通信的端点。
- 网络通信其实就是Socket间的通信。
- Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
- 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。
- Socket分类:
- 流套接字(stream socket):使用TCP提供可依赖的字节流服务
- 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务
Socket类的常用构造器:
- public Socket(InetAddress address,int port)创建一个流套接字并将其连接到指定IP 地址的指定端口号。
- public Socket(String host,int port)创建一个流套接字并将其连接到指定主机上的指定端口号。
Socket类的常用方法:
- public InputStream getInputStream()返回此套接字的输入流。可以用于接收网络消息
- public OutputStream getOutputStream()返回此套接字的输出流。可以用于发送网络消息
- public InetAddress getInetAddress()此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null。
- public InetAddress getLocalAddress()获取套接字绑定的本地地址。 即本端的IP地址
- public int getPort()此套接字连接到的远程端口号;如果尚未连接套接字,则返回 0。
- public int getLocalPort()返回此套接字绑定到的本地端口。 如果尚未绑定套接字,则返回 -1。即本端的 端口号。
- public void close()关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接 或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStream 和 OutputStream。
- public void shutdownInput()如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容,则流将 返回EOF(文件结束符)。 即不能在从此套接字的输入流中接收任何数据。
- public void shutdownOutput()禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发 送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用 shutdownOutput() 后写入套接字输出流, 则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。
TCP网络编程
- 基于Socket的TCP编程
Java语言的基于套接字编程分为服务端编程和客户端编程
客户端Socket的工作过程包含以下四个基本的步骤:
- 创建 Socket:根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
- 打开连接到Socket 的输入/出流: 使用 getInputStream()方法获得输入流,使用 getOutputStream()方法获得输出流,进行数据传输
- 按照一定的协议对Socket 进行读/写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程。
- 关闭 Socket:断开客户端到服务器的连接,释放线路
客户端创建Socket对象
- 客户端程序可以使用Socket类创建对象,创建的同时会自动向服务器方发起连 接。Socket的构造器是:
- Socket(String host,int port)throws UnknownHostException,IOException:向服务器(域名是host。端口号为port)发起TCP连接,若成功,则创建Socket对象,否则抛出异常。
- Socket(InetAddress address,int port)throws IOException:根据InetAddress对象所表示的 IP地址以及端口号port发起连接。
客户端建立socketAtClient对象的过程就是向服务器发出套接字连接请求
- 服务器程序的工作过程包含以下四个基本的步骤:
- 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口 上。用于监听客户端的请求。
- 调用 accept():监听连接请求,如果客户端请求连接,则接受连接,返回通信 套接字对象。
- 调用 该Socket类对象的 getOutputStream() 和 getInputStream ():获取输出流和输入流,开始网络数据的发送和接收。
- 关闭ServerSocket和Socket对象:客户端访问结束,关闭通信套接字。
- 服务器建立 ServerSocket 对象
- ServerSocket 对象负责等待客户端请求建立套接字连接,类似邮局某个窗口 中的业务员。也就是说,服务器必须事先建立一个等待客户请求建立套接字 连接的ServerSocket对象。
- 所谓“接收”客户的套接字请求,就是accept()方法会返回一个 Socket 对象
示例:客户端发送内容给服务端,服务端将内容打印到控制台上。
1 | import org.testng.annotations.Test; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
示例二:客户端发送文件给服务端,服务端将文件保存在本地。
1 | import org.testng.annotations.Test; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
练习3:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给 客户端。并关闭相应的连接。
1 | import org.testng.annotations.Test; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
UDP网络编程
UDP网络通信
- 类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。
- UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证
- UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
- DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP 地址和端口号以及接收端的IP地址和端口号。
- UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和 接收方的连接。如同发快递包裹一样。
DatagramSocket 类的常用方法
- public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。
- public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址。 本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地 址,IP 地址由内核选择。
- public void close()关闭此数据报套接字。
- public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
- public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法 在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的 长度长,该信息将被截短。
- public InetAddress getLocalAddress()获取套接字绑定的本地地址。
- public int getLocalPort()返回此套接字绑定的本地主机上的端口号。
- public InetAddress getInetAddress()返回此套接字连接的地址。如果套接字未连接,则返回null。
- public int getPort()返回此套接字的端口。如果套接字未连接,则返回 -1。
- public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长度为length 的数据包。 length 参数必须小于等于 buf.length。
- public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数 据报包,用来将长度为 length的包发送到指定主机上的指定端口号。length 参数必须小于等于buf.length。
- public InetAddress getAddress()返回某台机器的 IP 地址,此数据报将要发往该
机器或者是从该机器接收到的。 - public int getPort()返回某台远程主机的端口号,此数据报将要发往该主机或 者是从该主机接收到的。
- public byte[] getData()返回数据缓冲区。接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续length 长度。
- public int getLength()返回将要发送或接收到的数据的长度。
流 程:
- DatagramSocket与DatagramPacket
- 建立发送端,接收端
- 建立数据包
- 调用Socket的发送、接收方法
- 关闭Socket
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66import org.testng.annotations.Test;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UdpTest {
public void testSender() {
DatagramSocket socket = null;
try {
String str = new String("UDP发送数据");
byte[] data = str.getBytes();
InetAddress inet = InetAddress.getLocalHost();
socket = new DatagramSocket();
DatagramPacket packet = new DatagramPacket(data, 0, data.length, inet, 8888);
socket.send(packet);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public void testReceiver() throws IOException {
DatagramSocket socket = null;
try {
socket = new DatagramSocket(8888);
byte[] data = new byte[100];
DatagramPacket packet = new DatagramPacket(data, 0, data.length);
socket.receive(packet);
System.out.println(new String(packet.getData(), 0, packet.getLength()));
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}点击查看运行结果
URL编程
URL类
- URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址。
- 它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
- 通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。
URL的基本结构由5部分组成:
- <传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表
- 例 如 : http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123
- 片段名:即锚点,例如看小说,直接定位到章节
- 参数列表格式:参数名=参数值&参数名=参数值….
为了表示URL,java.net 中实现了类 URL。我们可以通过下面的构造器来初 始化一个 URL 对象:
- public URL (String spec):通过一个表示URL地址的字符串可以构造一个URL对象。例 如:URL url = new URL (“http://www. jermyn.cn/“);
- public URL(URL context, String spec):通过基 URL 和相对 URL 构造一个 URL 对象。例如:URL downloadUrl = new URL(url, “download.html”)
- public URL(String protocol, String host, String file); 例如:new URL(“http”, “www.jermyn.cn”, “download. html”);
- public URL(String protocol, String host, int port, String file); 例如: URL gamelan = new
URL(“http”, “www.jermyn.cn”, 80, “download.html”);
URL类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通 常是用 try-catch 语句进行捕获。
一个URL对象生成后,其属性是不能被改变的,但可以通过它给定的 方法来获取这些属性:
- public String getProtocol ( ) 获取该 URL 的协议名
- public String getHost ( ) 获取该URL的主机名
- public String getPort ( ) 获取该URL的端口号
- public String getPath ( ) 获取该URL的文件路径
- public String getFile ( ) 获取该URL的文件名
- public String getQuery ( ) 获取该URL的查询名
针对HTTP协议的URLConnection类
- URL的方法 openStream():能从网络上读取数据
- 若希望输出数据,例如向服务器端的 CGI (公共网关接口-Common Gateway Interface-的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一 些数据,则必须先与URL建立连接,然后才能对其进行读写,此时需要使用 URLConnection 。
URLConnection:表示到URL所引用的远程对象的连接。当与一个URL建立连接时, 首先要在一个 URL 对象上通过方法 openConnection() 生成对应的 URLConnection 对象。如果连接过程失败,将产生IOException.
- URL netchinaren = new URL (“http://www.jermyn.cn/index.shtml“);
- URLConnectonn u = netchinaren.openConnection( );
通过URLConnection对象获取的输入流和输出流,即可以与现有的CGI程序进行交互。
- public Object getContent( ) throws IOException
- public int getContentLength( )
- public String getContentType( )
- public long getDate( )
- public long getLastModified( )
- public InputStream getInputStream( )throws IOException
- public OutputSteram getOutputStream( )throws IOException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class URLTest {
public static void main(String[] args) {
HttpURLConnection urlConnection = null;
InputStream is = null;
FileOutputStream fos = null;
try {
URL url = new URL("$URL");
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.connect();
is = urlConnection.getInputStream();
fos = new FileOutputStream("$DEST_PATH");
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
System.out.println("下载完成");
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(urlConnection != null){
urlConnection.disconnect();
}
}
}
}
小结
- 位于网络中的计算机具有唯一的IP地址,这样不同的主机可以互相区分。
- 客户端-服务器是一种最常见的网络应用程序模型。服务器是一个为其客户端提供某种特定 服务的硬件或软件。客户机是一个用户应用程序,用于访问某台服务器提供的服务。端口号 是对一个服务的访问场所,它用于区分同一物理计算机上的多个服务。套接字用于连接客户 端和服务器,客户端和服务器之间的每个通信会话使用一个不同的套接字。TCP协议用于实 现面向连接的会话。
- Java 中有关网络方面的功能都定义在 java.net 程序包中。Java 用 InetAddress 对象表示 IP地址,该对象里有两个字段:主机名(String) 和 IP 地址(int)。
- 类 Socket 和 ServerSocket 实现了基于TCP协议的客户端-服务器程序。Socket是客户端 和服务器之间的一个连接,连接创建的细节被隐藏了。这个连接提供了一个安全的数据传输 通道,这是因为 TCP 协议可以解决数据在传送过程中的丢失、损坏、重复、乱序以及网络 拥挤等问题,它保证数据可靠的传送。
- 类 URL 和 URLConnection 提供了最高级网络应用。URL 的网络资源的位置来同一表示 Internet 上各种网络资源。通过URL对象可以创建当前应用程序和 URL 表示的网络资源之 间的连接,这样当前程序就可以读取网络资源数据,或者把自己的数据传送到网络上去。
第八章 Java反射机制
Java反射机制概述
Java Reflection
- Reflection(反射)是被视为动态语言的关键,反射机制允许程序在执行期 借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内 部属性及方法。
- 加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个 类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可 以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看 到类的结构,所以,我们形象的称之为:反射。
Java反射机制提供的功能
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时获取泛型信息
- 在运行时调用任意一个对象的成员变量和方法
- 在运行时处理注解
- 生成动态代理
理解Class类并获取Class的实例
Class 类
在Object类中定义了以下的方法,此方法将被所有子类继承:
- public final Class getClass()
以上的方法返回值的类型是一个Class类, 此类是Java反射的源头,实际上所谓反射 从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。
- 对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE都为其保留一个不变的Class类型的对象。一个Class对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。
- Class本身也是一个类
- Class 对象只能由系统建立对象
- 一个加载的类在 JVM 中只会有一个Class实例
- 一个Class对象对应的是一个加载到JVM中的一个.class文件
- 每个类的实例都会记得自己是由哪个 Class 实例所生成
- 通过Class可以完整地得到一个类中的所有被加载的结构
- Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的
- Class对象
获取Class类的实例(四种方法)
- 前提:若已知具体的类,通过类的class属性获取,该方法最为安全可靠, 程序性能最高
实例:Class clazz = String.class; - 前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象
实例:Class clazz = “www.atguigu.com”.getClass(); - 前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方 法forName()获取,可能抛ClassNotFoundException
实例:Class clazz = Class.forName(“java.lang.String”); - 其他方式(不做要求)
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass(“类的全类名”);示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import org.testng.annotations.Test;
public class ReflectionTest {
public void testGetClassInstance() throws ClassNotFoundException {
//方式一:调用运行时类的属性:.class
Class clazz1 = Person.class;
System.out.println(clazz1);
//方式二:通过运行时类的对象,调用getClass()
Person p1 = new Person();
Class clazz2 = p1.getClass();
System.out.println(clazz2);
//方式三:调用Class的静态方法:forName(String classPath)
Class clazz3 = Class.forName("cn.jermyn.test01.Person");
System.out.println(clazz3);
//方式四:使用类的加载器:ClassLoader (了解)
ClassLoader classLoader = ReflectionTest.class.getClassLoader();
Class clazz4 = classLoader.loadClass("cn.jermyn.test01.Person");
System.out.println(clazz4);
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12D:\PATH-EN\java-path\bin\java.exe ...
class cn.jermyn.test01.Person
class cn.jermyn.test01.Person
class cn.jermyn.test01.Person
===============================================
Default Suite
Total tests run: 1, Passes: 1, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
- 哪些类型可以有Class对象?
- class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
- interface:接口
- []:数组
- enum:枚举
- annotation:注解@interface
- primitive type:基本数据类型
- void
类的加载与ClassLoader的理解
- 类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过 如下三个步骤来对该类进行初始化。- 类的加载(Load):将类的class文件读入内存,并为之创建一 个java.lang.Class对象。此过程由类加载器完成
- 类的链接(Link):将类的二进制数 据合并到JRE中
- 类的初始化(Initialize):JVM负责对类 进行初始化
- 加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时 数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问 入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象。这个加载的 过程需要类加载器参与。
链接:将Java类的二进制代码合并到JVM的运行状态之中的过程。
- 验证:确保加载的类信息符合JVM规范,例如:以cafe开头,没有安全方面的问题
- 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存 都将在方法区中进行分配。
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化:
- 执行类构造器
()方法的过程。类构造器 ()方法是由编译期自动收集类中 所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信 息的,不是构造该类对象的构造器)。 - 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
- 虚拟机会保证一个类的
()方法在多线程环境中被正确加锁和同步。
- 执行类构造器
什么时候会发生类初始化?
类的主动引用(一定会发生类的初始化)
- 当虚拟机启动,先初始化main方法所在的类
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
类的被动引用(不会发生类的初始化)
- 当访问一个静态域时,只有真正声明这个域的类才会被初始化
- 当通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常 量池中了)
类加载器的作用:
- 类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方 法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为 方法区中类数据的访问入口。
- 类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器 中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
创建运行时类的对象
有了Class对象,能做什么?
- 创建类的对象:调用Class对象的newInstance()方法
- 要 求:
- 类必须有一个无参数的构造器。
- 类的构造器的访问权限需要足够。
难道没有无参的构造器就不能创建对象了吗?
不是!只要在操作的时候明确的调用类中的构造器,并将参数传递进去之后,才可以实例化操作。
步骤如下:- 通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类 型的构造器
- 向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
- 通过Constructor实例化对象。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51import org.testng.annotations.Test;
import java.util.Random;
public class NewInstanceTest {
public void testMethod() {
for (int i = 0; i < 10; i++) {
int num = new Random().nextInt(3);//0,1,2
String classPath = "";
switch (num) {
case 0:
classPath = "java.util.Date";
break;
case 1:
classPath = "java.lang.Object";
break;
case 2:
classPath = "cn.jermyn.test01.Person";
break;
}
try {
Object obj = getInstance(classPath);
System.out.println(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public Object getInstance(String classPath) throws Exception {
Class clazz = Class.forName(classPath);
return clazz.newInstance();
}
public void testMethod02() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Class<Person> aClass = (Class<Person>) Class.forName("cn.jermyn.test01.Person");
Person person = aClass.newInstance();
System.out.println(person);
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25D:\PATH-EN\java-path\bin\java.exe ...
----------------分割线----------------
Thu Oct 27 20:31:57 CST 2022
java.lang.Object@3abbfa04
Person()
Person{name='null', age=0}
Person()
Person{name='null', age=0}
Thu Oct 27 20:31:57 CST 2022
java.lang.Object@57fffcd7
Thu Oct 27 20:31:57 CST 2022
Thu Oct 27 20:31:57 CST 2022
Thu Oct 27 20:31:57 CST 2022
Thu Oct 27 20:31:57 CST 2022
----------------分割线----------------
Person()
Person{name='null', age=0}
===============================================
Default Suite
Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
获取运行时类的完整结构
通过反射获取运行时类的完整结构Field、Method、Constructor、Superclass、Interface、Annotation
实现的全部接口;所继承的父类;全部的构造器;全部的方法;全部的Field;
使用反射可以取得:
实现的全部接口
- public Class<?>[] getInterfaces()
确定此对象所表示的类或接口实现的接口。
- public Class<?>[] getInterfaces()
所继承的父类
- public Class<? Super T> getSuperclass()
返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的Class。
- public Class<? Super T> getSuperclass()
全部的构造器
- public Constructor
[] getConstructors()
返回此 Class 对象所表示的类的所有public构造方法。 public Constructor
[] getDeclaredConstructors()
此 Class 对象表示的类声明的所有构造方法。Constructor类中:
- 取得修饰符: public int getModifiers();
- 取得方法名称: public String getName();
- 取得参数的类型:public Class<?>[] getParameterTypes();
- public Constructor
全部的方法
- public Method[] getDeclaredMethods()
返回此Class对象所表示的类或接口的全部方法 public Method[] getMethods()
返回此Class对象所表示的类或接口的public的方法Method类中:
- public Class<?> getReturnType()取得全部的返回值
- public Class<?>[] getParameterTypes()取得全部的参数
- public int getModifiers()取得修饰符
- public Class<?>[] getExceptionTypes()取得异常信息
- public Method[] getDeclaredMethods()
全部的Field
- public Field[] getFields()
返回此Class对象所表示的类或接口的public的Field。 public Field[] getDeclaredFields()
返回此Class对象所表示的类或接口的全部Field。Field方法中:
- public int getModifiers()
以整数形式返回此Field的修饰符 - public Class<?> getType()
得到Field的属性类型 - public String getName()
返回Field的名称。
- public int getModifiers()
- public Field[] getFields()
Annotation相关
- get Annotation(Class
annotationClass) - getDeclaredAnnotations()
- get Annotation(Class
泛型相关
- 获取父类泛型类型:Type getGenericSuperclass()
- 泛型类型:ParameterizedType
- 获取实际的泛型类型参数数组:getActualTypeArguments()
类所在的包
- Package getPackage()
示例
1 | package cn.jermyn.test02; |
1 | package cn.jermyn.test02; |
1 | package cn.jermyn.test02; |
1 | package cn.jermyn.test02; |
1 | package cn.jermyn.test02; |
1 | package cn.jermyn.test02; |
1 | package cn.jermyn.test02; |
调用运行时类的指定结构
- 调用指定方法
通过反射,调用类中的方法,通过Method类完成。步骤:
- 通过Class类的getMethod(String name,Class…parameterTypes)方法取得一个Method对象,并设置此方法操作时所需要的参数类型。
- 之后使用Object invoke(Object obj, Object[] args)进行调用,并向方法中传递要设置的obj对象的参数信息。
Object invoke(Object obj, Object … args)说明:
- Object 对应原方法的返回值,若原方法无返回值,此时返回null
- 若原方法若为静态方法,此时形参Object obj可为null
- 若原方法形参列表为空,则Object[] args为null
- 若原方法声明为private,则需要在调用此invoke()方法前,显式调用 方法对象的setAccessible(true)方法,将可访问private的方法。
调用指定属性
在反射机制中,可以直接通过Field类操作类中的属性,通过Field类提供的set()和get()方法就可以完成设置和取得属性内容的操作。
- public Field getField(String name) 返回此Class对象表示的类或接口的指定的public的Field。
- public Field getDeclaredField(String name)返回此Class对象表示的类或接口的 指定的Field。
在Field中:
- public Object get(Object obj) 取得指定对象obj上此Field的属性内容
- public void set(Object obj,Object value) 设置指定对象obj上此Field的属性内容
关于setAccessible方法的使用
- Method和Field、Constructor对象都有setAccessible()方法。
- setAccessible启动和禁用访问安全检查的开关。
- 参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
- 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。
- 使得原本无法访问的私有成员也可以访问
- 参数值为false则指示反射的对象应该实施Java语言访问检查。
示例
1 | import cn.jermyn.test01.Person; |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
反射的应用:动态代理
静态代理
1 | interface ClothFactory { |
点击查看运行结果
1 | D:\PATH-EN\java-path\bin\java.exe ... |
- 代理设计模式的原理:使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原 始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原 始对象上。
- 动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时 根据需要动态创建目标类的代理对象。
动态代理使用场合:
- 调试
- 远程方法调用
动态代理相比于静态代理的优点:抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中 处理,这样,我们可以更加灵活和统一的处理众多的方法。
Java动态代理相关API
- Proxy :专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。
- 提供用于创建动态代理类和动态代理对象的静态方法
- static Class<?>getProxyClass(ClassLoader loader, Class<?>… interfaces) 创建一个动态代理类所对应的Class对象
- static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h) 直接创建一个动态代理对象
动态代理步骤
- 创建一个实现接口InvocationHandler的类,它必须实现invoke方法,以完成代理的具体操作。
- 创建被代理的类以及接口
- 通过Proxy的静态方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 创建一个Subject接口代理
- 通过 Subject代理调用RealSubject实现类的方法
动态代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Human {
String getBelief();
void eat(String food);
}
//2. 创建被代理的类以及接口
class SuperMan implements Human {
public String getBelief() {
return "Peace and love!";
}
public void eat(String food) {
System.out.println("我喜欢吃" + food);
}
}
class ProxyFactory {
public static Object getProxyInstance(Object obj) {
MyInvocationHandler handler = new MyInvocationHandler();
handler.bind(obj);
// 3.通过Proxy的静态方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 创建一个Subject接口代理
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler);
}
}
//1. 创建一个实现接口InvocationHandler的类,它必须实现invoke方法,以完成代理的具体操作。
class MyInvocationHandler implements InvocationHandler {
//obj:被代理类的对象
private Object obj;
public void bind(Object obj) {
this.obj = obj;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
//obj:被代理类的对象
Object invoke = method.invoke(obj, args);
//上述方法的返回值就作为当前类中的invoke()的返回值。
return invoke;
}
}
public class ProxyTest {
public static void main(String[] args) {
SuperMan superMan = new SuperMan();
//4. 通过ProxyFactory代理调用getProxyInstance实现类的方法
Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
String belief = proxyInstance.getBelief();
System.out.println(belief);
proxyInstance.eat("KFC");
}
}点击查看运行结果
1
2
3
4
5D:\PATH-EN\java-path\bin\java.exe ...
Peace and love!
我喜欢吃KFC
进程已结束,退出代码0
第九章 Java8的其它新特性
Lambda表达式
Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以 传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更 灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
Lambda 表达式:
在Java 8 语言中引入的一种新的语法元素和操 作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符 或箭头操作符。它将 Lambda 分为两个部分:
左侧:指定了 Lambda 表达式需要的参数列表
右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即Lambda 表达式要执行的功能。格式
语法格式一:无参,无返回值
语法格式二:Lambda 需要一个参数,但是没有返回值。
语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略
语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90import org.junit.Test;
import java.util.Comparator;
import java.util.function.Consumer;
/**
* Description:
* -> :lambda操作符 或 箭头操作符
* ->左边:lambda形参列表 (其实就是接口中的抽象方法的形参列表)
* ->右边:lambda体 (其实就是重写的抽象方法的方法体)
* Author: Jermyn
*/
public class LambdaTest {
// 语法格式一:无参,无返回值
public void test01() {
Runnable r1 = new Runnable() {
public void run() {
System.out.println("无参数,无返回值");
}
};
r1.run();
// 左边为形参列表,无则为(),右边方法体
Runnable r2 = () -> {
System.out.println("无参数,无返回值");
};
r2.run();
}
//语法格式二:Lambda 需要一个参数,但是没有返回值。
public void test02() {
Consumer<String> consumer = new Consumer<String>() {
public void accept(String s) {
System.out.println(s);
}
};
consumer.accept("To be,or not to be, that is the Question");
Consumer<String> con = (String s) -> {
System.out.println(s);
};
con.accept("生存即毁灭,死亡即永恒");
}
// 语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
public void test03() {
// 类型推断
Consumer<String> con = (str) -> {
System.out.println(str);
};
con.accept("生存即毁灭,死亡即永恒");
}
// 语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略
public void test04() {
// 省去str的小括号
Consumer<String> con = str -> {
System.out.println(str);
};
con.accept("生存即毁灭,死亡即永恒");
}
// 语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返回值
public void test05() {
Comparator<Integer> comparator = (x, y) -> {
return x.compareTo(y);
};
System.out.println(comparator.compare(12, 21));
}
// 语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略
public void test06() {
Comparator<Integer> comparator = (x, y) -> x.compareTo(y);
System.out.println(comparator.compare(21, 12));
}
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11D:\PATH-EN\java-path\bin\java.exe ...
无参数,无返回值
无参数,无返回值
To be,or not to be, that is the Question
生存即毁灭,死亡即永恒
生存即毁灭,死亡即永恒
生存即毁灭,死亡即永恒
-1
1
Process finished with exit code 0总结:
->左边:lambda形参列表的参数类型可以省略(类型推断);如果lambda形参列表只有一个参数,其一对()也可以省略
->右边:lambda体应该使用一对{}包裹;如果lambda体只有一条执行语句(可能是return语句),省略这一对{}和return关键字类型推断
上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序 的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于 上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”。
函数式(Functional)接口
什么是函数式(Functional)接口
- 只包含一个抽象方法的接口,称为函数式接口。
- 你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式 抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽 象方法上进行声明)。
- 我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检 查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个 接口是一个函数式接口。
- 在java.util.function包下定义了Java 8 的丰富的函数式接口
如何理解函数式接口
- Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP) 编程是一切。但是随着python、scala等语言的兴起和新技术的挑战,Java不 得不做出调整以便支持更加广泛的技术要求,也即java不但可以支持OOP还 可以支持OOF(面向函数编程)
- 在函数式编程语言当中,函数被当做一等公民对待。在将函数作为一等公民的 编程语言中,Lambda表达式的类型是函数。但是在Java8中,有所不同。在 Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的 对象类型——函数式接口。
- 简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是 Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口 的实例,那么该对象就可以用Lambda表达式来表示。
- 所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。
点击图片可以跳转源码
Java 内置四大核心函数式接口
其他接口
方法引用与构造器引用
方法引用(Method References)
- 当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!
- 方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就 是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向 一个方法,可以认为是Lambda表达式的一个语法糖。
- 要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的 方法的参数列表和返回值类型保持一致!
- 格式:使用操作符 “::” 将类(或对象)与方法名分隔开来。
- 如下三种主要使用情况:
- 对象::实例方法名
- 类::静态方法名
- 类::实例方法名
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112public class MethodRefTest {
// 情况一:对象 :: 实例方法
//Consumer中的void accept(T t)
//PrintStream中的void println(T t)
public void test1() {
Consumer<String> con1 = str -> System.out.println(str);
con1.accept("北京");
System.out.println("*******************");
PrintStream ps = System.out;
Consumer<String> con2 = ps::println;
con2.accept("beijing");
}
//Supplier中的T get()
//Employee中的String getName()
public void test2() {
Employee emp = new Employee(1001,"Tom",23,5600);
Supplier<String> sup1 = () -> emp.getName();
System.out.println(sup1.get());
System.out.println("*******************");
Supplier<String> sup2 = emp::getName;
System.out.println(sup2.get());
}
// 情况二:类 :: 静态方法
//Comparator中的int compare(T t1,T t2)
//Integer中的int compare(T t1,T t2)
public void test3() {
Comparator<Integer> com1 = (t1,t2) -> Integer.compare(t1,t2);
System.out.println(com1.compare(12,21));
System.out.println("*******************");
Comparator<Integer> com2 = Integer::compare;
System.out.println(com2.compare(12,3));
}
//Function中的R apply(T t)
//Math中的Long round(Double d)
public void test4() {
Function<Double,Long> func = new Function<Double, Long>() {
public Long apply(Double d) {
return Math.round(d);
}
};
System.out.println("*******************");
Function<Double,Long> func1 = d -> Math.round(d);
System.out.println(func1.apply(12.3));
System.out.println("*******************");
Function<Double,Long> func2 = Math::round;
System.out.println(func2.apply(12.6));
}
// 情况三:类 :: 实例方法 (有难度)
// Comparator中的int comapre(T t1,T t2)
// String中的int t1.compareTo(t2)
public void test5() {
Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2);
System.out.println(com1.compare("abc","abd"));
System.out.println("*******************");
Comparator<String> com2 = String :: compareTo;
System.out.println(com2.compare("abd","abm"));
}
//BiPredicate中的boolean test(T t1, T t2);
//String中的boolean t1.equals(t2)
public void test6() {
BiPredicate<String,String> pre1 = (s1,s2) -> s1.equals(s2);
System.out.println(pre1.test("abc","abc"));
System.out.println("*******************");
BiPredicate<String,String> pre2 = String :: equals;
System.out.println(pre2.test("abc","abd"));
}
// Function中的R apply(T t)
// Employee中的String getName();
public void test7() {
Employee employee = new Employee(1001, "Jerry", 23, 6000);
Function<Employee,String> func1 = e -> e.getName();
System.out.println(func1.apply(employee));
System.out.println("*******************");
Function<Employee,String> func2 = Employee::getName;
System.out.println(func2.apply(employee));
}
}
构造器引用
格式:ClassName::new- 与函数式接口相结合,自动与函数式接口中方法兼容。
- 可以把构造器引用赋值给定义的方法,要求构造器参数列表要与接口中抽象
- 方法的参数列表一致!且方法的返回值即为构造器对应类的对象。
数组引用
格式: type[] :: new1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68public class ConstructorRefTest {
//构造器引用
//Supplier中的T get()
//Employee的空参构造器:Employee()
public void test1(){
Supplier<Employee> sup = new Supplier<Employee>() {
public Employee get() {
return new Employee();
}
};
System.out.println("*******************");
Supplier<Employee> sup1 = () -> new Employee();
System.out.println(sup1.get());
System.out.println("*******************");
Supplier<Employee> sup2 = Employee :: new;
System.out.println(sup2.get());
}
//Function中的R apply(T t)
public void test2(){
Function<Integer,Employee> func1 = id -> new Employee(id);
Employee employee = func1.apply(1001);
System.out.println(employee);
System.out.println("*******************");
Function<Integer,Employee> func2 = Employee :: new;
Employee employee1 = func2.apply(1002);
System.out.println(employee1);
}
//BiFunction中的R apply(T t,U u)
public void test3(){
BiFunction<Integer,String,Employee> func1 = (id,name) -> new Employee(id,name);
System.out.println(func1.apply(1001,"Tom"));
System.out.println("*******************");
BiFunction<Integer,String,Employee> func2 = Employee :: new;
System.out.println(func2.apply(1002,"Tom"));
}
//数组引用
//Function中的R apply(T t)
public void test4(){
Function<Integer,String[]> func1 = length -> new String[length];
String[] arr1 = func1.apply(5);
System.out.println(Arrays.toString(arr1));
System.out.println("*******************");
Function<Integer,String[]> func2 = String[] :: new;
String[] arr2 = func2.apply(10);
System.out.println(Arrays.toString(arr2));
}
}
强大的Stream API
Stream API说明
- Java8中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则 是 Stream API。
- Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这 是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程 序员的生产力,让程序员写出高效率、干净、简洁的代码。
- Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进 行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。 也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种 高效且易于使用的处理数据的方式。
注意:
- Stream 自己不会存储元素。
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
Stream 的操作三个步骤
- 创建 Stream:一个数据源(如:集合、数组),获取一个流
- 中间操作:一个中间操作链,对数据源的数据进行处理
- 终止操作(终端操作):一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用
创建 Stream
方式一:通过集合Java8 中的 Collection 接口被扩展,提供了两个获取流 的方法:- default Stream
stream() : 返回一个顺序流 - default Stream
parallelStream() : 返回一个并行流
- default Stream
方式二:通过数组Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:
static
- public static IntStream stream(int[] array)
- public static LongStream stream(long[] array)
- public static DoubleStream stream(double[] array)
方式三:通过Stream的of()可以调用Stream类静态方法 of(), 通过显示值创建一个 流。它可以接收任意数量的参数。
- public static
Stream of(T… values) : 返回一个流
方式四:创建无限流可以使用静态方法 Stream.iterate() 和 Stream.generate(),
创建无限流。
- 迭代public static
Stream iterate(final T seed, final UnaryOperator f) - 生成public static
Stream generate(Supplier s) 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class StreamAPITest {
//创建 Stream方式一:通过集合
public void test01() {
List<Employee> employees = EmployeeData.getEmployees();
// default Stream<E> stream() : 返回一个顺序流
Stream<Employee> stream = employees.stream();
// default Stream<E> parallelStream() : 返回一个并行流
Stream<Employee> employeeStream = employees.parallelStream();
}
//创建 Stream方式二:通过数组
public void test02() {
int[] arr = new int[]{8, 2, 7, 3, 8};
IntStream stream = Arrays.stream(arr);
Employee e1 = new Employee(12, "Jermyn", 6000);
Employee e2 = new Employee(113, "Jack", 3000);
Employee[] employees = new Employee[]{e1, e2};
Stream<Employee> stream1 = Arrays.stream(employees);
}
//创建 Stream方式三:通过Stream的of()
public void test03() {
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
}
//创建 Stream方式四:创建无限流
public void test04() {
// 迭代:public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
//遍历前10个偶数
Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);
// 生成:public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}
}
- Stream 的中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。
筛选与切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void test01() {
List<Employee> employees = EmployeeData.getEmployees();
// filter(Predicate p)——接收 Lambda ,从流中排除某些元素。
Stream<Employee> stream = employees.stream();
stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println);
System.out.println();
// 第一个Stream已经终止
// limit(n)——截断流,使其元素不超过给定数量。
Stream<Employee> stream2 = employees.stream();
stream2.limit(5).forEach(System.out::println);
System.out.println();
// skip(n) —— 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
Stream<Employee> stream3 = employees.stream();
stream3.skip(1).forEach(System.out::println);
System.out.println();
// distinct()——筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
employees.add(new Employee(1010,"刘强东",40,8000));
employees.add(new Employee(1010,"刘强东",40,8000));
employees.add(new Employee(1010,"刘强东",40,8000));
// System.out.println(employees);
employees.stream().distinct().forEach(System.out::println);
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30D:\PATH-EN\java-path\bin\java.exe ...
Employee{id=1002, name='马云', age=12, salary=9876.12}
Employee{id=1004, name='雷军', age=26, salary=7657.37}
Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
Employee{id=1001, name='马化腾', age=34, salary=6000.38}
Employee{id=1002, name='马云', age=12, salary=9876.12}
Employee{id=1003, name='刘强东', age=33, salary=3000.82}
Employee{id=1004, name='雷军', age=26, salary=7657.37}
Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
Employee{id=1002, name='马云', age=12, salary=9876.12}
Employee{id=1003, name='刘强东', age=33, salary=3000.82}
Employee{id=1004, name='雷军', age=26, salary=7657.37}
Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
Employee{id=1007, name='任正非', age=26, salary=4333.32}
Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}
Employee{id=1001, name='马化腾', age=34, salary=6000.38}
Employee{id=1002, name='马云', age=12, salary=9876.12}
Employee{id=1003, name='刘强东', age=33, salary=3000.82}
Employee{id=1004, name='雷军', age=26, salary=7657.37}
Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
Employee{id=1007, name='任正非', age=26, salary=4333.32}
Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}
Employee{id=1010, name='刘强东', age=40, salary=8000.0}
Process finished with exit code 0映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void test02() {
// map(Function f)——接收一个函数作为参数,将元素转换成其他形式或提取信息,该函数会被应用到每个元素上,并将其映射成一个新的元素。
List<Employee> employees = EmployeeData.getEmployees();
Stream<Employee> stream = employees.stream();
Stream<String> stringStream = stream.map(e -> e.getName()).filter(name -> name.length() > 3);
stringStream.forEach(System.out::println);
System.out.println();
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest::fromStringToStream);
streamStream.forEach(e -> {
e.forEach(System.out::println);
});
System.out.println();
// flatMap(Function f)——接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
Stream<Character> characterStream = list.stream().flatMap(StreamAPITest::fromStringToStream);
characterStream.forEach(System.out::println);
}
//将字符串中的多个字符构成的集合转换为对应的Stream的实例
public static Stream<Character> fromStringToStream(String str) {//aa
ArrayList<Character> list = new ArrayList<>();
for (Character c : str.toCharArray()) {
list.add(c);
}
return list.stream();
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23D:\PATH-EN\java-path\bin\java.exe ...
比尔盖茨
扎克伯格
a
a
b
b
c
c
d
d
a
a
b
b
c
c
d
d
Process finished with exit code 0排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void test04() {
// 自然排序
List<Integer> integers = Arrays.asList(11, 12, 13, 23, 2, 3, 1);
integers.stream().sorted().forEach(System.out::println);
System.out.println();
// 定制排序
// 按照年龄从大到小排序
List<Employee> employees = EmployeeData.getEmployees();
Stream<Employee> stream = employees.stream();
stream.sorted((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge())).forEach(System.out::println);
System.out.println();
// 当年龄一样是按照薪水从大到小
Stream<Employee> stream1 = employees.stream();
stream1.sorted((e1, e2) -> {
int ageValue = Integer.compare(e1.getAge(), e2.getAge());
if (ageValue != 0) {
return ageValue;
} else {
return Double.compare(e1.getSalary(), e2.getSalary());
}
}).forEach(System.out::println);
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28D:\PATH-EN\java-path\bin\java.exe ...
1
2
3
11
12
13
23
Employee{id=1002, name='马云', age=12, salary=9876.12}
Employee{id=1004, name='雷军', age=26, salary=7657.37}
Employee{id=1007, name='任正非', age=26, salary=4333.32}
Employee{id=1003, name='刘强东', age=33, salary=3000.82}
Employee{id=1001, name='马化腾', age=34, salary=6000.38}
Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}
Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
Employee{id=1002, name='马云', age=12, salary=9876.12}
Employee{id=1007, name='任正非', age=26, salary=4333.32}
Employee{id=1004, name='雷军', age=26, salary=7657.37}
Employee{id=1003, name='刘强东', age=33, salary=3000.82}
Employee{id=1001, name='马化腾', age=34, salary=6000.38}
Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}
Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
Process finished with exit code 0
- 终止操作
- 终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。
- 流进行了终止操作后,不能再次使用。
匹配与查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81// 匹配与查找
public void test01() {
List<Employee> employees = EmployeeData.getEmployees();
Stream<Employee> stream = employees.stream();
// allMatch(Predicate p)——检查是否匹配所有元素。
// 练习:是否所有的员工的年龄都大于18
boolean b = stream.allMatch(employee -> employee.getAge() > 18);
System.out.println(b);
System.out.println();
// anyMatch(Predicate p)——检查是否至少匹配一个元素。
// 练习:是否存在员工的工资大于 10000
Stream<Employee> stream1 = employees.stream();
boolean b1 = stream1.anyMatch(employee -> employee.getSalary() > 10000);
System.out.println(b1);
System.out.println();
// noneMatch(Predicate p)——检查是否没有匹配的元素。
// 练习:是否**存在**员工姓“雷”
Stream<Employee> stream2 = employees.stream();
boolean lei = stream2.noneMatch(employee -> employee.getName().startsWith("雷"));
System.out.println(lei);
System.out.println();
// findFirst——返回第一个元素
Stream<Employee> stream3 = employees.stream();
Optional<Employee> first = stream3.findFirst();
System.out.println(first);
System.out.println();
// findAny——返回当前流中的任意元素
Stream<Employee> stream4 = employees.parallelStream();
Optional<Employee> any = stream4.findAny();
System.out.println(any);
}
public void test02() {
List<Employee> employees = EmployeeData.getEmployees();
// count——返回流中元素的总个数
Stream<Employee> stream = employees.stream();
long count = stream.count();
System.out.println(count);
System.out.println();
// max(Comparator c)——返回流中最大值
// 练习:返回最高的工资:
Stream<Employee> stream1 = employees.stream();
Stream<Double> doubleStream = stream1.map(employee -> employee.getSalary());
Optional<Double> max = doubleStream.max((e1, e2) -> Double.compare(e1, e2));
System.out.println(max);
System.out.println();
// min(Comparator c)——返回流中最小值
// 练习:返回最低工资的员工
Stream<Employee> stream2 = employees.stream();
Optional<Employee> min = stream2.min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
System.out.println(min);
System.out.println();
// forEach(Consumer c)——内部迭代
employees.stream().forEach(System.out::println);
System.out.println();
//使用集合的遍历操作
employees.forEach(System.out::println);
}点击查看运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35D:\PATH-EN\java-path\bin\java.exe ...
false
false
false
Optional[Employee{id=1001, name='马化腾', age=34, salary=6000.38}]
Optional[Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}]
8
Optional[9876.12]
Optional[Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}]
Employee{id=1001, name='马化腾', age=34, salary=6000.38}
Employee{id=1002, name='马云', age=12, salary=9876.12}
Employee{id=1003, name='刘强东', age=33, salary=3000.82}
Employee{id=1004, name='雷军', age=26, salary=7657.37}
Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
Employee{id=1007, name='任正非', age=26, salary=4333.32}
Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}
Employee{id=1001, name='马化腾', age=34, salary=6000.38}
Employee{id=1002, name='马云', age=12, salary=9876.12}
Employee{id=1003, name='刘强东', age=33, salary=3000.82}
Employee{id=1004, name='雷军', age=26, salary=7657.37}
Employee{id=1005, name='李彦宏', age=65, salary=5555.32}
Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
Employee{id=1007, name='任正非', age=26, salary=4333.32}
Employee{id=1008, name='扎克伯格', age=35, salary=2500.32}
Process finished with exit code 0归约
map 和 reduce 的连接通常称为 map-reduce 模式,因 Google用它来进行网络搜索而出名。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void test03() {
List<Integer> integers = Arrays.asList(1, 2, 3, 5, 6, 4);
// reduce(T identity, BinaryOperator)——可以将流中元素反复结合起来,得到一个值。返回 T
// 练习1:计算1-10的自然数的和
Stream<Integer> stream = integers.stream();
Integer reduce = stream.reduce(0, Integer::sum);
System.out.println(reduce);
System.out.println();
// reduce(BinaryOperator) ——可以将流中元素反复结合起来,得到一个值。返回 Optional<T>
// 练习2:计算公司所有员工工资的总和
List<Employee> employees = EmployeeData.getEmployees();
Optional<Double> reduce1 = employees.stream().map(employee -> employee.getSalary()).reduce(Double::sum);
System.out.println(reduce1);
}点击查看运行结果
1
2
3
4
5
6D:\PATH-EN\java-path\bin\java.exe ...
21
Optional[48424.08]
Process finished with exit code 0收集
Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、 Map)。1
2
3
4
5
6
7
8
9
10
11
public void test04() {
// collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
// 练习1:查找工资大于6000的员工,结果返回为一个List或Set
List<Employee> employees = EmployeeData.getEmployees();
Stream<Employee> stream = employees.stream();
Stream<Employee> employeeStream = stream.filter(employee -> employee.getSalary() > 6000);
List<Employee> collect = employeeStream.collect(Collectors.toList());
collect.forEach(System.out::println);
}点击查看运行结果
1
2
3
4
5
6
7D:\PATH-EN\java-path\bin\java.exe ...
Employee{id=1001, name='马化腾', age=34, salary=6000.38}
Employee{id=1002, name='马云', age=12, salary=9876.12}
Employee{id=1004, name='雷军', age=26, salary=7657.37}
Employee{id=1006, name='比尔盖茨', age=42, salary=9500.43}
Process finished with exit code 0
Optional类
- Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
创建Optional类对象的方法:
- Optional.of(T t) : 创建一个 Optional 实例,t必须非空;
- Optional.empty() : 创建一个空的 Optional 实例
- Optional.ofNullable(T t):t可以为null
判断Optional容器中是否包含对象:
- boolean isPresent() : 判断是否包含对象
- void ifPresent(Consumer<? super T> consumer) :如果有值,就执行Consumer 接口的实现代码,并且该值会作为参数传给它。
获取Optional容器的对象:
- T get(): 如果调用对象包含值,返回该值,否则抛异常
- T orElse(T other) :如果有值则将其返回,否则返回指定的other对象。
- T orElseGet(Supplier<? extends T> other) :如果有值则将其返回,否则返回由Supplier接口实现提供的对象。
- T orElseThrow(Supplier<? extends X> exceptionSupplier) :如果有值则将其返 回,否则抛出由Supplier接口实现提供的异常。
第十章 Java9&10&11新特性
Java 9 的新特性
JDK 和 JRE 目录结构的改变
- JDK 8 的目录结构
- JDK 9 的目录结构
- JDK 8 的目录结构
模块化系统: Jigsaw -> Modularity
本质上讲也就是说,用模块来管理各个package,通过声明某个package 暴露,,模块(module)的概念,其实就是package外再裹一层,不声明默 认就是隐藏。因此,模块化使得代码组织上更安全,因为它可以指定哪 些部分可以暴露,哪些部分隐藏。- 实现目标
- 模块化的主要目的在于减少内存的开销
- 只须必要模块,而非全部jdk模块,可简化各种类库和大型应用的开 发和维护
- 改进 Java SE 平台,使其可以适应不同大小的计算设备
- 改进其安全性,可维护性,提高性能
- 实现目标
模块将由通常的类和新的模块声明文件(module-info.java)组成。该文件是位于 java代码结构的顶层,该模块描述符明确地定义了我们的模块需要什么依赖关系,以及哪些模块被外部使用。在exports子句中未提及的所有包默认情况下将封装在模块中,不能在外部使用。
Java的REPL工具: jShell命令
- 实现目标
- Java 9 中终于拥有了 REPL工具:jShell。让Java可以像脚本语言一样运行,从 控制台启动jShell,利用jShell在没有创建类的情况下直接声明变量,计算表达式, 执行语句。即开发时可以在命令行里直接运行Java的代码,而无需创建Java文
件,无需跟人解释”public static void main(String[] args)”这句废话。 - jShell也可以从文件中加载语句或者将语句保存到文件中。
- jShell也可以是tab键进行自动补全和自动添加分号。
- Java 9 中终于拥有了 REPL工具:jShell。让Java可以像脚本语言一样运行,从 控制台启动jShell,利用jShell在没有创建类的情况下直接声明变量,计算表达式, 执行语句。即开发时可以在命令行里直接运行Java的代码,而无需创建Java文
- 实现目标
语法改进:接口的私有方法
Java 8中规定接口中的方法除了抽象方法之外,还可以定义静态方法 和默认的方法。一定程度上,扩展了接口的功能,此时的接口更像是 一个抽象类。
在Java9中,接口更加的灵活和强大,连方法的访问权限修饰符都可 以声明为private的了,此时方法将不会成为你对外暴露的API的一部分。
语法改进:钻石操作符使用升级
我们将能够与匿名实现类共同使用钻石操作符(diamond operator)在Java 8中如下的操作是会报错的:1
2
3
4
5
6Comparator<Object> com = new Comparator<>() {
public int compare(Object o1, Object o2) {
return 0;
}
};Java 9中如下操作可以正常执行通过:
1
2
3
4
5
6
7// anonymous classes can now use type inference
Comparator<Object> com = new Comparator<>(){
public int compare(Object o1, Object o2) {
return 0;
}
};语法改进:try语句
Java 8 中,可以实现资源的自动关闭,但是要求执行后必须关闭的所有资源必须在try子句中初始化,否则编译不通过。如下例所示:1
2
3
4
5try(InputStreamReader reader = new InputStreamReader(System.in)) {
//读取数据细节省略
} catch (IOException e) {
e.printStackTrace();
}Java 9中,用资源语句编写try将更容易,我们可以在try子句中使用已经初始 化过的资源,此时的资源是final的:
1
2
3
4
5
6
7
8
9InputStreamReader reader = new InputStreamReader(System.in);
OutputStreamWriter writer = new OutputStreamWriter(System.out); \
try (reader; writer) {
//reader是final的,不可再被赋值
//reader = null;
//具体读写操作省略
} catch (IOException e) {
e.printStackTrace();
}String存储结构变更
Motivation
The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.
Description
We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.
结论:String 再也不用 char[] 来存储,改成了 byte[] 加上编码标记,节约了一些空间。1
2
3
4public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final
}集合工厂方法:快速创建只读集合
要创建一个只读、不可改变的集合,必须构造和分配它,然后添加元素,最后 包装成一个不可修改的集合。1
2
3
4
5
6
7List<String> namesList = new ArrayList <>();
namesList.add("Joe");
namesList.add("Bob");
namesList.add("Bill");
namesList = Collections.unmodifiableList(namesList);
System.out.println(namesList);InputStream 加强
InputStream 终于有了一个非常有用的方法:transferTo,可以用来将数据直接 传输到 OutputStream,这是在处理原始数据流时非常常见的一种用法,如下示例。1
2
3
4
5
6
7ClassLoader cl = this.getClass().getClassLoader();
try (InputStream is = cl.getResourceAsStream("hello.txt");
OutputStream os = new FileOutputStream("src\\hello1.txt")) {
is.transferTo(os); // 把输入流中的所有数据直接自动地复制到输出流中
} catch (IOException e) {
e.printStackTrace();
}增强的 Stream API
- Java 的 Steam API 是java标准库最好的改进之一,让开发者能够快速运算, 从而能够有效的利用数据并行计算。Java 8 提供的 Steam 能够利用多核架构 实现声明式的数据处理。
- 在Java 9 中,Stream API 变得更好,Stream 接口中添加了 4 个新的方法: takeWhile, dropWhile, ofNullable,还有个 iterate 方法的新重载方法,可以 让你提供一个Predicate (判断条件)来指定什么时候结束迭代。
- 除了对 Stream 本身的扩展,Optional 和 Stream 之间的结合也得到了改进。 现在可以通过 Optional 的新方法 stream() 将一个 Optional 对象转换为一个 (可能是空的) Stream 对象。
- takeWhile()的使用
用于从 Stream 中获取一部分数据,接收一个 Predicate 来进行选择。在有序的Stream 中,takeWhile 返回从开头开始的尽量多的元素。1
2
3
4
5
6
7List<Integer> list = Arrays.asList(45, 43, 76, 87, 42, 77, 90, 73, 67, 88);
list.stream().takeWhile(x -> x < 50).forEach(System.out::println);
System.out.println();
list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
list.stream().takeWhile(x -> x < 5).forEach(System.out::println); - dropWhile()的使用
dropWhile 的行为与takeWhile 相反,返回剩余的元素。1
2
3
4
5List<Integer> list = Arrays.asList(45, 43, 76, 87, 42, 77, 90, 73, 67, 88);
list.stream().dropWhile(x -> x < 50).forEach(System.out::println);
System.out.println();
list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
list.stream().dropWhile(x -> x < 5).forEach(System.out::println); - ofNullable()的使用
Java 8 中 Stream 不能完全为null,否则会报空指针异常。而 Java 9 中的ofNullable方法允许我们创建一个单元素 Stream,可以包含一个非空元素,也可以创建一个空 Stream。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 报 NullPointerException
// Stream<Object> stream1 = Stream.of(null);
// System.out.println(stream1.count());
// 不报异常,允许通过
Stream<String> stringStream = Stream.of("AA", "BB", null);
System.out.println(stringStream.count());// 3
// 不报异常,允许通过
List<String> list = new ArrayList<>();
list.add("AA");
list.add(null);
System.out.println(list.stream().count());// 2
// ofNullable():允许值为null
Stream<Object> stream1 = Stream.ofNullable(null);
System.out.println(stream1.count());// 0
Stream<String> stream = Stream.ofNullable("hello world");
System.out.println(stream.count());// 1 - iterate()重载的使用
这个 iterate 方法的新重载方法,可以让你提供一个 Predicate (判断条件)来指定什么时候结束迭代。1
2
3
4// 原来的控制终止方式:
Stream.iterate(1, i -> i + 1).limit(10).forEach(System.out::println);
// 现在的终止方式:
Stream.iterate(1, i -> i < 100, i -> i + 1).forEach(System.out::println);
Optional获取Stream的方法
Optional类中stream()的使用1
2
3
4
5
6
7
8List<String> list = new ArrayList<>();
list.add("Tom");
list.add("Jerry");
list.add("Tim");
Optional<List<String>> optional = Optional.ofNullable(list);
Stream<List<String>> stream = optional.stream();
stream.flatMap(x -> x.stream()).forEach(System.out::println);Javascript引擎升级:Nashorn
- Nashorn 项目在 JDK 9 中得到改进,它为 Java 提供轻量级的 Javascript 运行时。 Nashorn 项目跟随 Netscape 的 Rhino 项目,目的是为了在 Java 中实现一个高 性能但轻量级的 Javascript 运行时。Nashorn 项目使得 Java 应用能够嵌入 Javascript。它在 JDK 8 中为Java 提供一个Javascript 引擎。
- JDK 9 包含一个用来解析 Nashorn 的 ECMAScript 语法树的 API。这个 API 使得 IDE 和服务端框架不需要依赖 Nashorn 项目的内部实现类, 就能够分析 ECMAScript 代码。
Java 10 新特性
局部变量类型推断
好处:减少了啰嗦和形式的代码,避免了信息冗余,而且对齐了变量名,更容易阅读!
场景一:类实例化时
作为 Java开发者,在声明一个变量时,我们总是习惯了敲打两次变量类型,第一次用于声明变量类型,第二次用于构造器。1
LinkedHashSet<Integer> set = new LinkedHashSet<>();
场景二:返回值类型含复杂泛型结构
变量的声明类型书写复杂且较长,尤其是加上泛型的使用1
Iterator<Map.Entry<Integer, Student>> iterator = set.iterator();
场景三:我们也经常声明一种变量,它只会被使用一次,而且是用在下一行代码中,比如:
1
2
3URL url = new URL("https://jermyn.cn/");
URLConnection connection = url.openConnection();
Reader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));尽管IDE可以帮我们自动完成这些代码,但当变量总是跳来跳去的时候,可读 性还是会受到影响,因为变量类型的名称由各种不同长度的字符组成。而且, 有时候开发人员会尽力避免声明中间变量,因为太多的类型声明只会分散注意 力,不会带来额外的好处。
局部变量类型推断适用于以下情况:
1
2
3
4
5
6
7
8
9
10
11
12//1.局部变量的初始化
var list = new ArrayList<>();
//2.增强for循环中的索引
for(var v : list) {
System.out.println(v);
}
//3.传统for循环中
for(var i = 0;i < 100;i++) {
System.out.println(i);
}局部变量类型推断如下情况不适用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void test() {
//1.局部变量不赋值,就不能实现类型推断
var num ;
//2.lambda表示式中,左边的函数式接口不能声明为var
Supplier<Double> sup = () -> Math.random();
var sup = () -> Math.random();
//3.方法引用中,左边的函数式接口不能声明为var
Consumer<String> con = System.out::println;
var con = System.out::println;
//4.数组的静态初始化中,注意如下的情况也不可以
int[] arr = {1, 2, 3, 4};
var arr = {1,2,3,4};
}不适用以下的结构中:
情况1:没有初始化的局部变量声明
情况2:方法的返回类型
情况3:方法的参数类型
情况4:构造器的参数类型
情况5:属性
情况6:catch块1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void test() {
// 情况1:没有初始化的局部变量声明
var s = null;
// 情况6:catch块
try{
}catch(var e){
e.printStackTrace();
}
//情况2:方法的返回类型
public var method1(){
return 0;
}
// 情况3:方法的参数类型
public void method2(var num){
}
//情况4:构造器的参数类型
public Java10Test(var i){
}
//情况5:属性
var num;
}工作原理
在处理 var时,编译器先是查看表达式右边部分,并根据右边变量值的类型进行 推断,作为左边变量的类型,然后将该类型写入字节码当中。注意
var不是一个关键字
你不需要担心变量名或方法名会与 var发生冲突,因为 var实际上并不是一个关键字,
而是一个类型名,只有在编译器需要知道类型的地方才需要用到它。除此之外,它 就是一个普通合法的标识符。也就是说,除了不能用它作为类名,其他的都可以, 但极少人会用它作为类名。
这不是JavaScript
首先我要说明的是,var并不会改变Java是一门静态类型语言的事实。编译器负责推 断出类型,并把结果写入字节码文件,就好像是开发人员自己敲入类型一样。集合新增创建不可变集合的方法
自 Java 9 开始,Jdk 里面为集合(List / Set / Map)都添加了 of (jdk9新增)和 copyOf (jdk10新增)方法,它们两个都用来创建不可变的集合,来看下它们的 使用和区别。1
2
3
4
5
6
7
8
9
10//示例1:
var list1 = List.of("Java", "Python", "C");
var copy1 = List.copyOf(list1);
System.out.println(list1 == copy1); // true
//示例2:
var list2 = new ArrayList<String>();
var copy2 = List.copyOf(list2);
System.out.println(list2 == copy2); // false
//示例1和2代码基本一致,为什么一个为true,一个为false?从源码分析 ,可以看出 copyOf方法会先判断来源集合是不是 AbstractImmutableList 类型的,如果是,就直接返回,如果不是,则调用 of 创 建一个新的集合。
示例2因为用的 new 创建的集合,不属于不可变 AbstractImmutableList 类的子类,
所以 copyOf 方法又创建了一个新的实例,所以为false。
注意:使用of和copyOf创建的集合为不可变集合,不能进行添加、删除、替换、 排序等操作,不然会报 java.lang.UnsupportedOperationException 异常。
上面演示了 List 的 of 和copyOf 方法,Set 和Map 接口都有。
Java 11 新特性
新增了一系列字符串处理方法
Optional 加强
Optional 也增加了几个非常酷的方法,现在可以很方便的将一个 Optional 转换 成一个 Stream, 或者当一个空 Optional 时给它一个替代的。局部变量类型推断升级
在var上添加注解的语法格式,在jdk10中是不能实现的。在JDK11中加入了这样的语法。1
2
3
4
5
6
7//错误的形式: 必须要有类型, 可以加上var
//Consumer<String> con1 = (@Deprecated t) ->
System.out.println(t.toUpperCase());
//正确的形式:
//使用var的好处是在使用lambda表达式时给参数加上注解。
Consumer<String> con2 = (var t) -> System.out.println(t.toUpperCase());全新的HTTP 客户端API
- HTTP,用于传输网页的协议,早在1997年就被采用在目前的1.1版本中。直 到2015年,HTTP2才成为标准。
- HTTP/1.1和HTTP/2的主要区别是如何在客户端和服务器之间构建和传输数据。 HTTP/1.1依赖于请求/响应周期。 HTTP/2允许服务器“push”数据:它可以发 送比客户端请求更多的数据。这使得它可以优先处理并发送对于首先加载 网页至关重要的数据。
- 这是 Java 9 开始引入的一个处理 HTTP 请求的的 HTTP Client API,该 API 支持同步和异步,而在 Java 11 中已经为正式可用状态,你可以在
- java.net 包中找到这个 API。
它将替代仅适用于blocking模式的HttpURLConnection(HttpURLConnection是在HTTP 1.0的时代创建的,并使用了协议无关的 方法),并提供对WebSocket 和 HTTP/2的支持。
更简化的编译运行程序
看下面的代码。1
2
3
4
5// 编译
javac Javastack.java
// 运行
java Javastack在我们的认知里面,要运行一个 Java 源代码必须先编译,再运行,两步执行动作。而在未来的Java 11版本中,通过一个 java 命令就直接搞定了,如以下所示:
1
java Javastack.java
一个命令编译运行源代码的注意点:
- 执行源文件中的第一个类, 第一个类必须包含主方法。
- 并且不可以使用其它源文件中的自定义类, 本文件中的自定义类是可以使用的。
废弃Nashorn引擎
废除Nashorn javascript引擎,在后续版本准备移除掉,有需要的可以考虑使用GraalVM。ZGC
- GC是java主要优势之一。 然而, 当GC停顿太长, 就会开始影响应用的响应时 间。消除或者减少GC停顿时长, java将对更广泛的应用场景是一个更有吸引力 的平台。此外, 现代系统中可用内存不断增长,用户和程序员希望JVM能够以高 效的方式充分利用这些内存, 并且无需长时间的GC暂停时间。
- ZGC, A Scalable Low-Latency Garbage Collector(Experimental) ZGC, 这应该是JDK11最为瞩目的特性, 没有之一。 但是后面带了Experimental, 说明这还不建议用到生产环境。
- ZGC是一个并发, 基于region, 压缩型的垃圾收集器, 只有root扫描阶段会 STW(stop the world), 因此GC停顿时间不会随着堆的增长和存活对象的增长 而变长。
优势:
- GC暂停时间不会超过10ms
- 既能处理几百兆的小堆, 也能处理几个T的大堆(OMG)
- 和G1相比, 应用吞吐能力不会下降超过15%
- 为未来的GC功能和利用colord指针以及Load barriers优化奠定基础
- 初始只支持64位系统
ZGC的设计目标是:
支持TB级内存容量,暂停时间低(<10ms),对整个 程序吞吐量的影响小于15%。 将来还可以扩展实现机制,以支持不少令人 兴奋的功能,例如多层堆(即热对象置于DRAM和冷对象置于NVMe闪存),或压缩堆。其它新特性
- Unicode 10
- Deprecate the Pack200 Tools and API
- 新的Epsilon垃圾收集器
- 完全支持Linux容器(包括Docker)
- 支持G1上的并行完全垃圾收集
- 最新的HTTPS安全协议TLS 1.3
- Java Flight Recorder