洋蔥

贪婪,找不到比这更好的词了,是件好事。

Java中与线程安全有关的类:安全的集合类、java.util.concurrent JUC包下类 等。

集合类

线程安全(Thread-safe)的集合对象:

  • Vector:单个操作是原子性的。但是如果两个原子操作复合而来,这个组合的方法是非线程安全的,需要使用锁来保证线程安全。
  • HashTable
  • CopyOnWriteArrayList:JUC包

如何把一个线程不安全的集合类变成一个线程安全的集合类?

1
2
3
4
5
6
7
public class ThreadDemo {
public static void main(String[] args) {
// Vector是线程安全的时候才去考虑使用的,但即使要安全,也不用
List<String> list1 = new ArrayList<String>();// 线程不安全
List<String> list2 = Collections.synchronizedList(list1); // 线程安全
}
}

字符串类

StringBuffer

原子类

CAS

包含三个操作数:当前内存值、预期原值、新值,如果内存值和预期原值匹配,就将内存值更改为新值;否则什么也不做。

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
public class Test {
public static volatile int i = 0;

public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
int expectedValue = i;
System.out.println("期望值: " + expectedValue);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// CAS操作
boolean result = compareAndSwap(i, expectedValue, i + 1);
System.out.println("设置是否成功: " + result);
System.out.println("新值: " + i);
}
};
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
i = 5;
}

public static boolean compareAndSwap(int memoryValue, int expectedValue, int newValue) {
// 当预期原值等于内存值时
if (expectedValue == memoryValue) {
i = memoryValue;
return true;
}
return false;
}
}

ABA问题

假如一个值原来是A,现在变成了B,后来又变成了A,那么CAS检查时会发现它的值没有发生变化,但是实际上却变化了。

AtomicMarkableReference 是一个带修改标记的引用类型原子类。

AtomicStampedReference 是一个带版本号的引用类型原子类。

Lock接口

JUC.locks包

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Lock {
// 这四个方法都可以获取锁
void lock();
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void lockInterruptibly() throws InterruptedException;

// 释放锁
void unlock();

// 用于等待通知(线程间通信)
Condition newCondition();
}

ReentrantLock

java.util.concurrent.locks.ReentrantLock

这个是 JDK @since 1.5 添加的一种颗粒度更小的锁,它完全可以替代 synchronized 关键字来实现它的所有功能,而且 ReentrantLock 锁的灵活度要远远大于 synchronized 关键字,它更清晰的表达了如何加锁释放锁

image-20210714185046829

lock() 阻塞

获取锁,平常使用最多的一个方法。

如果其他线程没有持有锁,则获取该锁并立即返回,将锁持有计数设置为 1。

如果当前线程已经持有锁,那么持有计数加一并且该方法立即返回。

如果该锁被另一个线程持有,那么当前线程将因线程调度目的而被禁用并处于休眠状态(等待获取锁),直到获得该锁为止,此时锁持有计数设置为 1。

注意:

  • 必须主动去释放锁。
  • 发生异常时,不会自动释放锁。所以使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
1
2
3
4
5
6
7
8
9
10
11
12
private final Lock lock = new XxxLock();

public void doSomething() {
lock.lock(); // 获取锁,阻塞
try {
// ... 同步代码
} catch (Exception e) {

} finally {
lock.unlock(); // 释放锁
}
}

示例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
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
thread0.start();
thread1.start();
}
}

class Task implements Runnable {
private Lock lock = new ReentrantLock();

@Override
public void run() {
lock.lock(); // 获取锁,阻塞
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}

示例2:

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
class SellTicket implements Runnable {
// 定义票
private int tickets = 1;
// 定义锁对象
private Lock lock = new ReentrantLock();

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
lock.lock(); // 获取锁,阻塞
try {
if (tickets <= 100) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets++) + "张票");
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();// 释放锁
}
}
}
}

tryLock() 非阻塞

tryLock() 有返回值,它表示用来尝试获取锁。

如果其他线程没有持有锁,则获取该锁并立即返回true值,将锁持有计数设置为 1。即使此锁已设置为使用公平排序策略,调用tryLock()将立即获取可用的锁,无论其他线程当前是否正在等待该锁。这种“闯入”行为在某些情况下很有用,即使它破坏了公平。 如果你想尊重这个锁的公平性设置,那么使用 tryLock(0, TimeUnit.SECONDS) 这几乎是等效的(它也检测中断)。

如果当前线程已持有此锁,则持有计数将增加 1 并且该方法返回 true。

如果锁被另一个线程持有,那么这个方法将立即返回 false 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final Lock lock = new XxxLock();

public void doSomething() {
if (lock.tryLock()) { // 获取锁,非阻塞
try {
// ... 同步代码
} catch (Exception e) {

} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("锁被占用,执行其他任务。");
}
}

tryLock(time,unit) 定时阻塞,可中断

如果在给定的等待时间内成功获取锁(没有被另一个线程持有),且当前线程没有被中断,则返回true,将锁持有计数设置为 1。

由于该方法的声明中抛出了异常,所以它必须放在try块中或者在调用的方法上抛出 InterruptedException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private final Lock lock = new XxxLock();

public void doSomething() throws InterruptedException {
try {
if (lock.tryLock(1000, TimeUnit.SECONDS)) { // 定时获取锁,定时阻塞,可中断
try {
// ... 同步代码
} catch (Exception e) {

} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("锁被占用,执行其他任务。");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
}
}

示例:

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
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
thread0.start();
thread1.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断线程
thread1.interrupt();
}
}

class Task implements Runnable {
private Lock lock = new ReentrantLock();

@Override
public void run() {
try {
if (lock.tryLock(1000, TimeUnit.SECONDS)) { // 定时获取锁,定时阻塞,可中断
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " 执行完成");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " sleep时被中断");
} finally {
lock.unlock(); // 释放锁
}
} else {
System.out.println("锁被占用,执行其他任务。");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
}
}
}

lockInterruptibly() 阻塞,可中断

这个方法比较特殊,当通过该方法获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时使用 lock.lockInterruptibly() 获取某个锁时,假若线程A获取到了锁,线程B在等待,那么对线程B调用 threadB.interrupt() 方法能够中断线程B的等待过程。

由于该方法的声明中抛出了异常,所以它必须放在try块中或者在调用的方法上抛出 InterruptedException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final Lock lock = new XxxLock();

public void doSomething() {
try {
lock.lockInterruptibly(); // 获取锁,阻塞,可中断
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
return;
}

try {
// ... 同步代码
} catch (Exception e) {

} finally {
lock.unlock(); // 释放锁
}
}

示例:

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
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread0 = new Thread(task);
Thread thread1 = new Thread(task);
thread0.start();
thread1.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断线程
thread1.interrupt();
}
}

class Task implements Runnable {
private Lock lock = new ReentrantLock();

@Override
public void run() {
try {
lock.lockInterruptibly(); // 获取锁,阻塞,可中断
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
return;
}

try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " 执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}

其他特有方法

1
2
3
4
public final boolean isFair() // 判断锁是否是公平锁
public boolean isLocked() // 查询此锁是否被任何线程持有。此方法设计用于监视系统状态,而不是用于同步控制。
public boolean isHeldByCurrentThread() // 查询当前线程是否持有此锁。
public final boolean hasQueuedThreads() // 查询是否有线程正在等待获取此锁。请注意,取消可能随时发生,如果可能有其他线程等待获取锁,则返回true。该方法主要设计用于监视系统状态。

ReentrantReadWriteLock

image-20210714204748943

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;

public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }

public static class ReadLock implements Lock, java.io.Serializable {
// ...
}
public static class WriteLock implements Lock, java.io.Serializable {
// ...
}
}

ReentrantReadWriteLock 实现了 ReadWriteLock 接口(非 Lock 接口),调用该接口的 readLock()writeLock() 方法可分别获取读和写的 Lock 锁。

ReentrantReadWriteLock 还提供了丰富的用于监视系统状态的方法。

为什么要分两把锁?

因为读操作的锁,要支持被多个线程获取(多线程同时读),这样可以提升读操作的效率。而写操作的锁,只能被一个线程获取。

读锁被占用时,无法申请写锁,但能申请读锁。

写锁被占用时,无法申请写锁,无法申请读锁。

读读的过程是共享的,读写、写读 、写写 的过程是互斥的。

ReentrantReadWriteLock读写锁详解

读写锁实战高并发容器

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
public class Test {
public static void main(String[] args) {
// 创建高并发容器
ReadWriteDictionary dictionary = new ReadWriteDictionary();
// 创建读、写任务
WriteTask writeTask = new WriteTask(dictionary);
ReadTask readTask = new ReadTask(dictionary);
// 创建读、写线程
Thread writeThread0 = new Thread(writeTask);
Thread writeThread1 = new Thread(writeTask);
Thread writeThread2 = new Thread(writeTask);
Thread readThread3 = new Thread(readTask);
Thread readThread4 = new Thread(readTask);
Thread readThread5 = new Thread(readTask);
// 启动线程
writeThread0.start();
writeThread1.start();
writeThread2.start();
readThread3.start();
readThread4.start();
readThread5.start();
}
}

/**
* 从容器读取数据的任务
*/
class ReadTask implements Runnable {
private ReadWriteDictionary dictionary;

public ReadTask(ReadWriteDictionary dictionary) {
this.dictionary = dictionary;
}

@Override
public void run() {
// 无限读取
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取所有key
String[] keys = dictionary.allKeys();
// 遍历所有的key
for (String key : keys) {
Object value = dictionary.get(key);
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "读取 >>> " + key + ": " + value);
}
}
}
}

/**
* 向容器写入数据的任务
*/
class WriteTask implements Runnable {
private ReadWriteDictionary dictionary;

public WriteTask(ReadWriteDictionary dictionary) {
this.dictionary = dictionary;
}

@Override
public void run() {
// 计数器
int i = 0;
// 无限写入
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = Thread.currentThread().getName() + "写入" + i++;
// 存储数据
dictionary.put("固定key", value);
}
}
}

/**
* 高并发容器
*/
class ReadWriteDictionary {
/**
* key-value容器
*/
private final Map<String, Object> map = new HashMap<>();

/**
* 读写锁
*/
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

/**
* 读锁
*/
private final Lock readLock = readWriteLock.readLock();

/**
* 写锁
*/
private final Lock writeLock = readWriteLock.writeLock();

public Object get(String key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}

/**
* 返回所有键
*/
public String[] allKeys() {
readLock.lock();
try {
return map.keySet().toArray(new String[0]);
} finally {
readLock.unlock();
}
}

public Object put(String key, Object value) {
writeLock.lock();
try {
return map.put(key, value);
} finally {
writeLock.unlock();
}
}

public void clear() {
writeLock.lock();
try {
map.clear();
} finally {
writeLock.unlock();
}
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Thread-3读取 >>> 固定key: Thread-0写入1
Thread-5读取 >>> 固定key: Thread-0写入1
Thread-4读取 >>> 固定key: Thread-0写入1
Thread-3读取 >>> 固定key: Thread-1写入2
Thread-5读取 >>> 固定key: Thread-1写入2
Thread-4读取 >>> 固定key: Thread-1写入2
Thread-3读取 >>> 固定key: Thread-2写入3
Thread-4读取 >>> 固定key: Thread-2写入3
Thread-5读取 >>> 固定key: Thread-2写入3
Thread-3读取 >>> 固定key: Thread-1写入4
Thread-4读取 >>> 固定key: Thread-1写入4
Thread-5读取 >>> 固定key: Thread-1写入4
Thread-3读取 >>> 固定key: Thread-1写入5
Thread-4读取 >>> 固定key: Thread-1写入5
Thread-5读取 >>> 固定key: Thread-1写入5
Thread-3读取 >>> 固定key: Thread-1写入6
Thread-4读取 >>> 固定key: Thread-1写入6
Thread-5读取 >>> 固定key: Thread-1写入6
Thread-3读取 >>> 固定key: Thread-0写入7
Thread-5读取 >>> 固定key: Thread-2写入7
Thread-4读取 >>> 固定key: Thread-2写入7

可重入性

读锁 写锁
读锁 可重入 不可重入
写锁 不可重入 不可重入

和 StampedLock 相同。验证读写不可重入:

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 Test {
public static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public static final Lock readLock = readWriteLock.readLock();
public static final Lock writeLock = readWriteLock.writeLock();

public static void main(String[] args) {
printA();
}

private static void printA() {
readLock.lock();
try {
System.out.println("printA");
printB();
} finally {
readLock.unlock();
}
}

private static void printB() {
writeLock.lock();
try {
System.out.println("printB");
} finally {
writeLock.unlock();
}
}
}

LockSupport工具类

实际开发中用的不多。可以用 LockSupport实现互斥锁(等待唤醒机制)

方法介绍:

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
public class LockSupport {
private LockSupport() {} // 无法实例化

/*********** 无限等待 ************/
// 使当前线程等待,直到唤醒或中断
public static void park();
// 使当前线程等待,并指定同步锁,直到唤醒或中断
public static void park(Object blocker);

/*********** 定时等待***********/
// 使当前线程等待,并指定等待时间,直到唤醒或中断或超时
public static void parkNanos(long nanos);
// 使当前线程等待,并指定同步锁和等待时间,直到唤醒或中断或超时
public static void parkNanos(Object blocker, long nanos);
// 使当前线程等待,并指定截止日期,直到唤醒或超时
public static void parkUntil(long deadline);
// 使当前线程等待,并指定同步锁和截止日期,直到唤醒或中断或超时
public static void parkUntil(Object blocker, long deadline);

// 唤醒指定线程
public static void unpark(Thread thread);

// 获取指定线程的同步锁
public static Object getBlocker(Thread t);
}

示例

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
public class Test {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task);
thread.start();

try {
thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
LockSupport.unpark(thread);
}
}


/**
* 从容器读取数据的任务
*/
class Task implements Runnable {
@Override
public void run() {
System.out.println("等待前");
LockSupport.park();
System.out.println("等待后");
}
}

StampedLock邮戳锁

线程饥饿

读写锁存在线程饥饿的问题。饥饿指的是某一线程或多个线程因为某些原因一直获取不到资源,导致程序一直无法执行。场景举例:

1、某一线程因优先级太低导致一直分配不到资源。

2、某一线程一直占着某种资源不放,导致其他线程长期无法执行。如:读(写)操作长时间占用读(写)锁,导致写(读)操作一直等待。

如何解决线程饥饿问题:

1、与死锁相比,饥饿现象有可能在一段时间后恢复执行。可以设置合适的线程优先级,来尽量避免饥饿的产生。

2、使用读写锁的升级版 StampedLock 来解决线程饥饿。

和ReadWriteLock相比

ReadWriteLock可以解决多线程同时读,但同时只能有一个线程写的问题。潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。

要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock

StampedLockReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。

乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

常用方法

方法 描述
public StampedLock() 只有一个无参构造
public long readLock() 获取读锁,返回锁标识
public long writeLock() 获取写锁,返回锁标识
public void unlock(long stamp) 释放读写锁,参数stamp为锁标识
public void unlockRead(long stamp) 释放读锁,参数stamp为锁标识
public void unlockWrite(long stamp) 释放写锁,参数stamp为锁标识
public Lock asReadLock() 转换为读锁
public Lock asWriteLock() 转换为写锁
public ReadWriteLock asReadWriteLock() 转换为读写锁

示例

饥饿问题复现

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
public class Test {
public static void main(String[] args) {
Data data = new Data();
ReadTask readTask = new ReadTask(data);
WriteTask writeTask = new WriteTask(data);
Thread thread0 = new Thread(readTask);
Thread thread1 = new Thread(writeTask);
thread0.start();
thread1.start();
}
}

class ReadTask implements Runnable {
private Data data;

public ReadTask(Data data) {
this.data = data;
}

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println(data.getMessage());
}
}
}

class WriteTask implements Runnable {
private Data data;

public WriteTask(Data data) {
this.data = data;
}

@Override
public void run() {
// 计数器
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
data.setMessage("当前值: " + i++);
}
}
}

class Data {
private String message;
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();

public String getMessage() {
readLock.lock();
try {
Thread.sleep(1000);
return this.message;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
readLock.unlock();
}
}

public void setMessage(String message) {
writeLock.lock();
try {
Thread.sleep(1000);
this.message = message;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}

输出结果用于最后的对比

1
2
3
4
5
6
7
8
9
10
11
null
当前值: 26
当前值: 30
当前值: 45
当前值: 63
当前值: 64
当前值: 84
当前值: 92
当前值: 128
当前值: 131
当前值: 134

解决饥饿问题

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
class Data {
private String message;
private final StampedLock stampedLock = new StampedLock();

public String getMessage() {
// 获取读锁标识
long readStamp = stampedLock.readLock();
try {
Thread.sleep(1000);
return this.message;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
// 释放读锁
stampedLock.unlockRead(readStamp);
}
}

public void setMessage(String message) {
// 获取写锁标识
long writeStamp = stampedLock.writeLock();
try {
Thread.sleep(1000);
this.message = message;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
stampedLock.unlockWrite(writeStamp);
}
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
null
当前值: 51
当前值: 72
当前值: 136
当前值: 152
当前值: 168
当前值: 182
当前值: 205
当前值: 227
当前值: 230
当前值: 260

对比输出结果

输出10行,发现 StampedLock 输出到260,而 ReentrantReadWriteLock 输出到 134,是否能证明饥饿问题的出现和解决呢???

可重入性

和 ReentrantReadWriteLock 相同。验证读写不可重入:

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 Test {
public static StampedLock stampedLock = new StampedLock();

public static void main(String[] args) {
printA();
}

private static void printA() {
long stamp = stampedLock.readLock();
try {
System.out.println("printA");
printB();
} finally {
stampedLock.unlockRead(stamp);
}
}

private static void printB() {
long stamp = stampedLock.writeLock();
try {
System.out.println("printB");
} finally {
stampedLock.unlockWrite(stamp);
}
}
}

ThreadLocal

remove()

即便不调用 remove() 也不会导致内存溢出,只不过垃圾回收器压力大些。设置 JVM 参数 -Xms30m -Xmx30m ,用 VisualVM 观察垃圾回收次数,VisualVM也会占用 JVM 内存,所以不能设置太小。

验证代码如下,观察 VisualVM 中的垃圾回收次数 collections:

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
/**
* JVM参数:-Xms30m -Xmx30m
*/
public class Test {
public static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
// 执行次数
public static AtomicInteger cnt = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
// 这里休眠10s,待visualVM连接后继续
TimeUnit.SECONDS.sleep(10);

ExecutorService executorService =
new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, new SynchronousQueue<>(), new DiscardPolicy());
for (int i = 1000; i > 0; i--) {
executorService.submit(new Task());
TimeUnit.MILLISECONDS.sleep(10);
}

executorService.shutdown();
}
}

class Task implements Runnable {
private int _1MB = 1024 * 1024 * 1;

@Override
public void run() {
Byte[] byt = new Byte[_1MB];
Test.threadLocal.set(byt);
System.out.println(Test.cnt.incrementAndGet());
// Test.threadLocal.remove();
}
}

InheritableThreadLocal

InheritableThreadLocal 可继承的线程本地变量。如线程B被线程A创建,B就可以获取A中的 InheritableThreadLocal。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {
threadLocal.set("Hello World!");
new ThreadB().start();
}
}

class ThreadB extends Thread {
@Override
public void run() {
System.out.println(Test.threadLocal.get());
}
}

TransmittableThreadLocal

https://juejin.cn/post/7214901105977671717

继承自 InheritableThreadLocal。该类由阿里提供。

java中与线程安全有关的关键字:volatile、synchronized

volatile

Java中Volatile关键字详解

可见性

volatile修饰的变量不允许线程内部缓存(保证可见性),即直接修改内存,所以对其他线程是可见的。

变量经过 volatile 修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使其他处理器缓存数据的内存地址无效。

  1. 对于写操作:对变量更改完之后,要立刻写回到主存中。
  2. 对于读操作:对变量读取的时候,要从主存中读,而不是缓存。

问题来了,既然它可以保证修改的值立即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?

比如 volatile int a = 0; 之后有一个操作 a++; 这个变量a具有可见性,但是 a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

Java中只有对基本类型变量的赋值和读取是原子操作,如 i = 1; 的赋值操作,但是像 j = i; 或者 i++; 这样的操作都不是原子操作,存在线程安全问题。比如先读取 i 的值,再将 i 的值赋值给 j ,两个原子操作加起来就不是原子操作了。

所以,一个变量被volatile修饰,保证了每次读取的变量值是最新的。像变量自增这样的非原子操作,这个变量就不具有原子性了。

有序性

JVM 在 volatile 读写前后插入内存屏障,禁止特定指令重排。

可见性示例

示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("ThreadA开始执行...");
while (flag) { // 等同于 while (true)
// Thread.yield(); // 其实这里sleep或yield一下就不需要用volatile修饰了
if (!flag) {
System.out.println("跳出循环");
break;
}
}
System.out.println("ThreadA结束执行。。。");
}, "ThreadA").start();

TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("标识已经变更");
}
}

输出:

1
2
ThreadA开始执行...
标识已经变更

示例2

示例1变形,改变 while 循环体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("ThreadA开始执行...");
while (flag) {
System.out.println("跳出循环");
}
System.out.println("ThreadA结束执行。。。");
}, "ThreadA").start();

TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("标识已经变更");
}
}

输出:

1
2
3
4
5
6
ThreadA开始执行...
跳出循环
...
跳出循环
标识已经变更
ThreadA结束执行。。。

示例3

继续改变 while 循环体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("ThreadA开始执行...");
int i = 0;
while (flag) {
i++;
}
System.out.println("ThreadA结束执行。。。");
}, "ThreadA").start();

TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("标识已经变更");
}
}

输出:

1
2
ThreadA开始执行...
标识已经变更

总结

有以上 3 个示例可得出,flag 的可见性会根据 while 循环体内容儿变化,不知道为什么,请指教。

所以为了保证 flag 可见,用 volatile 修饰。

非原子性示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
// volatile不保证原子性
// 原子性:保证数据一致性、完整性
private static volatile int num = 0;

public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 2; j++) {
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
Test.num++;
}
}, String.valueOf(j)).start();
}

// 后台默认两个线程:一个是main线程,一个是gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "final num result = " + Test.num);
Thread.sleep(5000); // 由于不了解Thread.activeCount(),担心还有线程未执行完,所以休眠后再输出一次,事实证明Thread.activeCount()可靠
// 如果volatile保证原子性的话,最终的结果应该是20000。但是每次程序执行结果都不等于20000。
System.out.println(Thread.currentThread().getName() + "final num result = " + Test.num);
}
}

输出:

1
2
3
main final num result = 17114
main final num result = 20000
main final num result = 19317

三次执行,都是不同的结果。

为什么会出现这种呢?因为 num++ 被拆分成3个指令:

  • 执行 getfield 拿到主内存中的原始值 num。
  • 执行 iadd 进行加1操作。
  • 执行 putfield 把工作内存中的值写回主内存中。

当多个线程并发执行 putfield 指令的时候,会出现写回主内存覆盖问题,所以才会导致最终结果不为 20000,所以 volatile 不能保证原子性。

总结

  • volatile可用于内存访问的可见性,保证每次读的都是最新写的数据
  • volatile不具备原子性,原子功能需要使用Atomic类
  • 原子操作和原子操作合并之后,不是原子操作。

synchronized

什么是同步锁

sychronized 采取的同步策略互斥同步

在每次获取资源之前,都需要检查是否有线程占用该资源。已经进入的线程尚未执行完,将会阻塞后面其他线程。

同步锁的本质是对象实例。多线程安全需保证每个线程使用的是同一个锁对象

同步锁保证多线程下的每个操作都是原子性的。

同步锁、同步监听对象、同步监视器、互斥锁 它们都是同步锁,说的是一个意思。

弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

1、对象是什么?

我们可以随便创建一个对象试试。

2、需要同步的代码是哪些?

多条语句操作共享数据的代码部分给包起来

注意:

  • 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。

  • 这里的锁对象可以是任意对象。多个线程必须是同一把锁

使用方式

同步代码块(对象锁、类锁)

格式:

1
2
3
4
5
6
7
8
9
10
11
synchronized(对象) { // 对象锁
// 需要被同步的代码;
}

synchronized(this) { // 对象锁
// 需要被同步的代码;
}

synchronized(类.class) { // 类锁
// 需要被同步的代码;
}

优化 SellTicket :

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
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

class SellTicket extends Stock implements Runnable {

/**
* 创建锁对象
*/
private Object objLock = new Object();

@Override
public void run() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
// synchronized (new Object()) 不同锁对象
// synchronized (this) 对象锁
// synchronized (Object.class) 类锁
synchronized (objLock) {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
stopWatch.stop();
System.out.println(Thread.currentThread().getName() + "总耗时: " + stopWatch.getTotalTimeNanos());
}
}

同步方法(对象锁)

把同步加在方法上。

这里的锁对象是this

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
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

class SellTicket extends Stock implements Runnable {

@Override
public void run() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
sell();
}
stopWatch.stop();
System.out.println(Thread.currentThread().getName() + "总耗时: " + stopWatch.getTotalTimeNanos());
}

// 下面两个sell方法的锁对象相同
private synchronized void sell() {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void sell() {
synchronized (this) {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t" +
"\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

静态同步方法(类锁)

把同步加在方法上。只需要在同步方法上加一个 static

这里的锁对象是当前类的Class对象

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
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st1, "窗口1");
Thread t2 = new Thread(st2, "窗口2");
Thread t3 = new Thread(st3, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

class SellTicket extends Stock implements Runnable {

@Override
public void run() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
sell();
}
stopWatch.stop();
System.out.println(Thread.currentThread().getName() + "总耗时: " + stopWatch.getTotalTimeNanos());
}

// 类锁
private static synchronized void sell() {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 类锁
private void sell() {
synchronized (this.getClass()) {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t" +
"\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

Double-Check单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton(); // 1
}
}
}
return singleton; // 2
}
}
// Double-Check的目的是延迟初始化单例的一种方法
// 使用volatile关键字的主要目的是防止指令重排序。如果volatile不加入,jvm可以对指令进行重排,与类锁无关,过程1可能被拿到过程2进行延迟初始化,这样就违背了Double-Check目的与原则。
// 第一层if判断是为了减少多次synchronized,减少每次都同步,提升性能
// 第二层if为了防止同时锁后,进行多个实例创建

原子性

原子是世界上的最小单位,具有不可分割性。

比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。

非原子操作都会存在线程安全问题,需要我们使用同步技术(synchronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。

如果把一个事务看作是一个程序,要么全部执行,要么全不执行。这种特性就叫原子性。

在 Java 中 synchronized 和在 lock、unlock 中的操作保证了原子性。

concurrent 包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

扩展链接:

什么是原子性,什么是原子性操作?

[Redis的单个操作是原子性的,多个操作支持事务](https://www.runoob.com/redis/redis-intro.html#:~:text=原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。)

可见性

可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程修改共享变量后,还没有来的及将缓存中的变量刷新到主存,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。

可见性,指线程间内存的可见性一个线程的修改,对另一个线程立马可见

在 Java 中 volatile、synchronized 实现可见性。

可见性问题示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static boolean stopped = false;
//public static volatile boolean stopped = false; // volatile关键字保证可见性

public static void main(String[] args) {
new Thread(() -> {
// 当stopped为true时停止循环
while (!stopped) {
//Thread.sleep(0); // 也可以获取到stopped状态的改变
}
}).start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 停止循环
stopped = true;
}
}

有序性

指令重排 (Instruction Reordering):编译器 / JVM / CPU 出于性能优化目的调整执行顺序(打破有序性的一种手段)。

Java 语言提供了 volatilesynchronized 两个关键字来保证线程之间操作的有序性。

volatile

volatile 变量在写入和读取时,会插入特定的内存屏障(memory barrier),从而:

  • 禁止特定类型的指令重排序(写前不能有写,读后不能有写);
  • 保证对变量的可见性(一个线程写入,另一个线程马上能看到);
  • ⚠️ 不保证原子性(这点常被忽视)。

具体表现:

  • 写入 volatile:JVM 会在后面插入 StoreStore + StoreLoad 屏障;
  • 读取 volatile:JVM 会在前面插入 LoadLoad + LoadStore 屏障;
  • 这些屏障会让前后的操作 不能重排,因此达成“部分顺序性保证”。

synchronized

synchronized 的并发控制来自 Java 的“互斥锁语义”:同一时刻,最多只有一个线程持有某个对象的锁。

synchronized隐式地具备内存屏障语义

  • 进入同步块(lock acquire)时:会清空当前线程的工作内存,重新从主内存读取;
  • 退出同步块(lock release)时:会把本地工作内存刷新到主内存;
  • 这两步确保了同步块之间对同一锁的操作是 顺序可见的强 happen-before 关系)。

什么是线程安全

所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。

判断线程安全

判断一个程序是否存在线程安全问题的依据

  • 是否有多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据
阅读全文 »

Thread类

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是,Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由 Java 去调用这样的东西,然后提供一些类(Thread 类)供我们使用。我们就可以实现多线程程序了。

阅读全文 »

调度模型 - 百度

调度模型

线程有两种调度模型:

  • 分时调度模型:让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片。
  • 抢占式调度模型:java虚拟机采用该方式,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。
阅读全文 »

File

介绍

​ File:文件和目录(文件夹)路径名的抽象表示形式

​ IO流操作中大部分都是对文件的操作,所以Java就提供了File类供我们来操作文件

构造方法

1
2
3
  public File(String pathname):通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。
  public File(String parent, String child):根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
  public File(File parent, String child):根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FileDemo {
public static void main(String[] args) {
// File(String pathname):根据一个路径得到File对象
// 把e:\\demo\\a.txt封装成一个File对象
File file = new File("G:\\demo\\a.txt");
System.out.println(file);// G:\demo\a.txt

// File(String parent, String child):根据一个目录和一个子文件/目录得到File对象
File file2 = new File("G:\\demo", "a.txt");
System.out.println(file2);// G:\demo\a.txt

// File(File parent, String child):根据一个父File对象和一个子文件/目录得到File对象
File file3 = new File("G:\\demo");
File file4 = new File(file3, "a.txt");
System.out.println(file4);// G:\demo\a.txt

// 以上三种方式其实效果一样
}
}

File类功能

更多功能详见 API。

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
  A:创建功能:
    public boolean createNewFile() throws IOException
    public boolean mkdir()
    public boolean mkdirs()

  B:删除功能:
    public boolean delete():删除此抽象路径名表示的文件或目录。

  C:重命名功能
    public boolean renameTo(File dest)

  D:判断功能
    public boolean isDirectory():测试此抽象路径名表示的文件是否是一个目录。
    public boolean isFile():测试此抽象路径名表示的文件是否是一个标准文件。
    public boolean exists():测试此抽象路径名表示的文件或目录是否存在。
    public boolean canRead():测试应用程序是否可以读取此抽象路径名表示的文件。 是否可读
    public boolean canWrite():测试应用程序是否可以修改此抽象路径名表示的文件。 是否可写
    public boolean isHidden():测试此抽象路径名指定的文件是否是一个隐藏文件。

  E:获取功能
    public File getAbsoluteFile():返回此抽象路径名的绝对路径名形式。
    public String getAbsolutePath():返回此抽象路径名的绝对路径名字符串。
    public String getPath():将此抽象路径名转换为一个路径名字符串。获取相对路径。
    public String getName():返回由此抽象路径名表示的文件或目录的名称。
    public long length():返回由此抽象路径名表示的文件的长度(字节数)。如果此路径名表示一个目录,则返回值是不确定的。
    public long lastModified():返回此抽象路径名表示的文件最后一次被修改的时间(毫秒值)。

  F:高级获取功能
    public String[] list():返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录。 获取指定目录下的所有文件或者文件夹的名称数组
    public File[] listFiles():返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。 获取指定目录下的所有文件或者文件夹的File数组

  G:过滤器功能
    public String[] list(FilenameFilter filter):返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录。
    public File[] listFiles(FilenameFilter filter):返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。
    public File[] listFiles(FileFilter filter):返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。
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
/*
* 创建功能:
*/
public class FileDemo {
public static void main(String[] args) throws IOException {
// 需求:我要在G盘目录下创建一个文件夹demo
File file = new File("G:\\demo");
System.out.println("mkdir:" + file.mkdir());

// 需求:我要在G盘目录demo下创建一个文件a.txt
File file2 = new File("G:\\demo\\a.txt");
System.out.println("createNewFile:" + file2.createNewFile());

// 需求:我要在G盘目录test下创建一个文件b.txt
// Exception in thread "main" java.io.IOException: 系统找不到指定的路径。
// 注意:要想在某个目录下创建内容,该目录首先必须存在。
// File file3 = new File("G:\\test\\b.txt");
// System.out.println("createNewFile:" + file3.createNewFile());

// 需求:我要在G盘目录test下创建aaa目录
// File file4 = new File("G:\\test\\aaa");
// System.out.println("mkdir:" + file4.mkdir());

// File file5 = new File("G:\\test");
// File file6 = new File("G:\\test\\aaa");
// System.out.println("mkdir:" + file5.mkdir());
// System.out.println("mkdir:" + file6.mkdir());

// 其实我们有更简单的方法
// File file7 = new File("G:\\aaa\\bbb\\ccc\\ddd");
// System.out.println("mkdirs:" + file7.mkdirs());

// 看下面的这个东西:
// File file8 = new File("G:\\liuyi\\a.txt\\aaa");
// System.out.println("mkdirs:" + file8.mkdirs());// 只能创建文件夹
}
}
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 boolean delete()
*
* 注意:
* A:如果你创建文件或者文件夹忘了写盘符路径,那么,默认在项目路径下。
* B:Java中的删除不走回收站。
* C:如果此路径名表示一个目录(文件夹),则该目录必须为空才能删除。
*/
public class FileDemo {
public static void main(String[] args) throws IOException {
// 创建文件
// File file = new File("e:\\a.txt");
// System.out.println("createNewFile:" + file.createNewFile());

// 不写盘符路径时,默认在项目路径下
File file = new File("a.txt");
System.out.println("createNewFile:" + file.createNewFile());
System.out.println("delete:" + file.delete());

File file2 = new File("aaa\\bbb\\ccc");
System.out.println("mkdirs:" + file2.mkdirs());

// 删除功能:我要删除aaa文件夹,必须先删ccc文件夹,再删bbb文件夹,最后才能删除aaa文件夹
// File file4 = new File("aaa\\bbb\\ccc");
// File file6 = new File("aaa\\bbb");
File file7 = new File("aaa");
// System.out.println("delete:" + file4.delete());
// System.out.println("delete:" + file6.delete());
System.out.println("delete:" + file7.delete());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 重命名功能:public boolean renameTo(File dest)
* 如果路径名相同,就是改名。
* 如果路径名不同,就是改名并剪切。
*
* 路径以盘符开始:绝对路径 c:\\a.txt
* 路径不以盘符开始:相对路径 a.txt
*/
public class FileDemo {
public static void main(String[] args) throws IOException {
// 创建一个文件对象
// File file = new File("林青霞.jpg");
// System.out.println(file.createNewFile());
// 需求:我要修改这个文件的名称为"东方不败.jpg"
// File newFile = new File("东方不败.jpg");
// System.out.println("renameTo:" + file.renameTo(newFile));

File file2 = new File("东方不败.jpg");
File newFile2 = new File("G:\\林青霞.jpg");
System.out.println("renameTo:" + file2.renameTo(newFile2));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 判断功能:
*/
public class FileDemo {
public static void main(String[] args) throws IOException {
// 创建文件对象
File file = new File("a.txt");
System.out.println(file.createNewFile());

System.out.println("isDirectory:" + file.isDirectory());// false
System.out.println("isFile:" + file.isFile());// true
System.out.println("exists:" + file.exists());// true
System.out.println("canRead:" + file.canRead());// true
System.out.println("canWrite:" + file.canWrite());// true
System.out.println("isHidden:" + file.isHidden());// false
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 获取功能:
*/
public class FileDemo {
public static void main(String[] args) throws IOException {
// 创建文件对象
File file = new File("test.txt");
file.createNewFile();

System.out.println("getAbsolutePath:" + file.getAbsolutePath()); // E:\WorkspacesOxygen\DemosSpace\JavaProject\test.txt
System.out.println("getPath:" + file.getPath()); // test.txt
System.out.println("getName:" + file.getName()); // test.txt
System.out.println("length:" + file.length()); // 0
System.out.println("lastModified:" + file.lastModified()); // 1491468425566

// 1491468425566
Date d = new Date(1491468425566L);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String s = sdf.format(d);
System.out.println(s);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 高级获取功能:
*/
public class FileDemo {
public static void main(String[] args) {
// 指定一个目录
File file = new File("G:\\");

// public String[] list():获取指定目录下的所有文件或者文件夹的名称数组
String[] strArray = file.list();
for (String s : strArray) {
System.out.println(s);
}
System.out.println("------------");

// public File[] listFiles():获取指定目录下的所有文件或者文件夹的File数组
File[] fileArray = file.listFiles();
for (File f : fileArray) {
System.out.println(f.getName());
}
}
}

案例

  A:输出指定目录下指定后缀名的文件名称

    a:先获取所有的,在遍历的时候判断,再输出

    b:先判断,再获取,最后直接遍历输出即可

  B:批量修改文件名称

案例A

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
/*
* 判断E盘目录下是否有后缀名为.jpg的文件,如果有,就输出此文件名称
*
* 分析:
* A:封装e判断目录
* B:获取该目录下所有文件或者文件夹的File数组
* C:遍历该File数组,得到每一个File对象,然后判断
* D:是否是文件
* 是:继续判断是否以.jpg结尾
* 是:就输出该文件名称
* 否:不搭理它
* 否:不搭理它
*/
public class FileDemo {
public static void main(String[] args) {
// 封装e判断目录
File file = new File("e:\\");

// 获取该目录下所有文件或者文件夹的File数组
File[] fileArray = file.listFiles();

// 遍历该File数组,得到每一个File对象,然后判断
for (File f : fileArray) {
// 是否是文件
if (f.isFile()) {
// 继续判断是否以.jpg结尾
if (f.getName().endsWith(".jpg")) {
// 就输出该文件名称
System.out.println(f.getName());
}
}
}
}
}
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
/*
* 判断E盘目录下是否有后缀名为.jpg的文件,如果有,就输出此文件名称
* A:先获取所有的,然后遍历的时候,依次判断,如果满足条件就输出。
* B:获取的时候就已经是满足条件的了,然后输出即可。要想实现这个效果,就必须学习一个接口:文件名称过滤器
*
* public String[] list(FilenameFilter filter)
* public File[] listFiles(FilenameFilter filter)
*/
public class FileDemo {
public static void main(String[] args) {
// 封装e判断目录
File file = new File("G:\\");

// 获取该目录下所有文件或者文件夹的String数组
// public String[] list(FilenameFilter filter)
String[] strArray = file.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
// return false;
// return true;
// 通过这个测试,我们就知道了,到底把这个文件或者文件夹的名称加不加到数组中,取决于这里的返回值是true还是false
// 所以,这个的true或者false应该是我们通过某种判断得到的
// System.out.println(dir + "---" + name);
// File file = new File(dir, name);
// // System.ot.println(file);
// boolean flag = file.isFile();
// boolean flag2 = name.endsWith(".jpg");
// return flag && flag2;
return new File(dir, name).isFile() && name.endsWith(".jpg");
}
});

// 遍历
for (String s : strArray) {
System.out.println(s);
}
}
}

案例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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
* 需求:把E:\评书\三国演义下面的视频名称修改为
* 00?_介绍.avi
*
* 思路:
* A:封装目录
* B:获取该目录下所有的文件的File数组
* C:遍历该File数组,得到每一个File对象
* D:拼接一个新的名称,然后重命名即可
*/
public class FileDemo {
public static void main(String[] args) {
// 封装目录
File srcFolder = new File("G:\\评书\\三国演义");

// 获取该目录下所有的文件的File数组
File[] fileArray = srcFolder.listFiles();

// 遍历该File数组,得到每一个File对象
for (File file : fileArray) {
// System.out.println(file);
// E:\评书\三国演义\三国演义_001_[评书网-今天很高兴,明天就IO了]_桃园三结义.avi
// 改后:E:\评书\三国演义\001_桃园三结义.avi
String name = file.getName(); // 三国演义_001_[评书网-今天很高兴,明天就IO了]_桃园三结义.avi

int index = name.indexOf("_");
String numberString = name.substring(index + 1, index + 4);
// System.out.println(numberString);

// int startIndex = name.lastIndexOf('_');
// int endIndex = name.lastIndexOf('.');
// String nameString = name.substring(startIndex + 1, endIndex);
// System.out.println(nameString);
int endIndex = name.lastIndexOf('_');
String nameString = name.substring(endIndex);

String newName = numberString.concat(nameString); // 001_桃园三结义.avi
// System.out.println(newName);

File newFile = new File(srcFolder, newName); // E:\\评书\\三国演义\\001_桃园三结义.avi

// 重命名即可
file.renameTo(newFile);
}
}
}

递归

​ 方法定义中调用方法本身的现象

注意事项

  A:要有出口,否则就是死递归

  B:次数不能过多,否则内存溢出

  C:构造方法不能递归使用

案例

  A:递归求阶乘

  B:兔子问题

  C:递归输出指定目录下所有指定后缀名的文件绝对路径

  D:递归删除带内容的目录(小心使用)

案例A:

img

img

案例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
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
/**
* 有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问第二十个月的兔子对数为多少?
* 分析:我们要想办法找规律
* 兔子对数
* 第一个月: 1
* 第二个月: 1
* 第三个月: 2
* 第四个月: 3
* 第五个月: 5
* 第六个月: 8
* ...
*
* 由此可见兔子对象的数据是:
* 1,1,2,3,5,8...
* 规则:
* A:从第三项开始,每一项是前两项之和
* B:而且说明前两项是已知的
*
* 如何实现这个程序呢?
* A:数组实现
* B:变量的变化实现
* C:递归实现
*
* 假如相邻的两个月的兔子对数是a,b
* 第一个相邻的数据:a=1,b=1
* 第二个相邻的数据:a=1,b=2
* 第三个相邻的数据:a=2,b=3
* 第四个相邻的数据:a=3,b=5
* 看到了:下一次的a是以前的b,下一次是以前的a+b
*/
public class DiGuiDemo2 {
public static void main(String[] args) {
/************************ A:数组实现 ************************/
// 定义一个数组
int[] arr = new int[20];
arr[0] = 1;
arr[1] = 1;
// arr[2] = arr[0] + arr[1];
// arr[3] = arr[1] + arr[2];
// ...
for (int x = 2; x < arr.length; x++) {
arr[x] = arr[x - 2] + arr[x - 1];
}
System.out.println(arr[19]); // 6765
/************************ A:数组实现 ************************/

/************************ B:变量的变化实现 ************************/
int a = 1;
int b = 1;
for (int x = 0; x < 18; x++) {
// 临时变量存储上一次的a
int temp = a;
a = b;
b = temp + b;
}
System.out.println(b); // 6765
/************************ B:变量的变化实现 ************************/

/************************ C:递归实现 ************************/
System.out.println(fib(20, 1)); // 6765
/************************ C:递归实现 ************************/
}

/**
*
* @param mouth
* 月数
* @param num
* 初始兔子数量
* @return mouth 月后的兔子数量
*/
public static int fib(int mouth, int num) {
if (mouth == 1 || mouth == 2) {
return num;
} else {
return fib(mouth - 1, num) + fib(mouth - 2, num);
}
}
}

案例C:

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
/**
* 需求:请大家把G:\JavaSE目录下所有的java结尾的文件的绝对路径给输出在控制台。
*
* 分析:
* A:封装目录
* B:获取该目录下所有的文件或者文件夹的File数组
* C:遍历该File数组,得到每一个File对象
* D:判断该File对象是否是文件夹
* 是:回到B
* 否:继续判断是否以.java结尾
* 是:就输出该文件的绝对路径
* 否:不搭理它
*/
public class FilePathDemo {
public static void main(String[] args) {
// 封装目录
File srcFolder = new File("G:\\JavaSE");

// 递归功能实现
getAllJavaFilePaths(srcFolder);
}

private static void getAllJavaFilePaths(File srcFolder) {
// 获取该目录下所有的文件或者文件夹的File数组
File[] fileArray = srcFolder.listFiles();// 如果是根目录,有些有些受保护的文件调用该方法会返回null。自己可以加个判断,不是null再去遍历即可。

// 遍历该File数组,得到每一个File对象
for (File file : fileArray) {
// 判断该File对象是否是文件夹
if (file.isDirectory()) {
getAllJavaFilePaths(file);
} else {
// 继续判断是否以.java结尾
if (file.getName().endsWith(".java")) {
// 就输出该文件的绝对路径
System.out.println(file.getAbsolutePath());
}
}
}
}
}

案例D:

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
/**
* 需求:递归删除带内容的目录
*
* 目录我已经给定:demo
*
* 分析:
* A:封装目录
* B:获取该目录下的所有文件或者文件夹的File数组
* C:遍历该File数组,得到每一个File对象
* D:判断该File对象是否是文件夹
* 是:回到B
* 否:就删除
*/
public class FileDeleteDemo {
public static void main(String[] args) {
// 封装目录
File srcFolder = new File("G:\\JavaSE");
// 递归实现
deleteFolder(srcFolder);
}

private static void deleteFolder(File srcFolder) {
// 获取该目录下的所有文件或者文件夹的File数组
File[] fileArray = srcFolder.listFiles();

if (fileArray != null) {
// 遍历该File数组,得到每一个File对象
for (File file : fileArray) {
// 判断该File对象是否是文件夹
if (file.isDirectory()) {
deleteFolder(file);
} else {
System.out.println(file.getName() + "---" + file.delete());
}
}

System.out.println(srcFolder.getName() + "---" + srcFolder.delete());
} else {
System.out.println("该目录不存在!");
}
}
}

作用

​ IO流用来处理设备之间的数据传输(上传文件和下载文件)。

  Java对数据的操作的通过流的方式。

  Java用于操作流的对象都在IO包中。

流的分类

一般我们在讨论IO流的时候,如果没有明确说明按照什么分,默认按照数据类型分。

  A:数据流向(方向)

    输入流 取数据

    输出流 出数据

  B:数据类型(单位)

    字节流(操作二进制文件,用Windows记事本打开读不懂的文件,英文也可以用字节流)

      字节输入流 InputStream

        |– FileInputStream

        |– BufferedInputStream

      字节输出流 OutputStream

        |– FileOutputStream

        |– BufferedOutputStream

    字符流(操作文本文件,用Windows记事本打开能读懂的文件,包括中文、英文等各国语言)

      字符输入流 Reader

        |– InputStreamReader

          |– FileReader

        |– BufferedReader

      字符输出流 Writer

        |– OutputStreamWriter

          |– FileWriter

        |– BufferedWriter

img

  C:功能

    节点流,管道流(处理流)

输入流与输出流的区别

​ 1).无论文件是否存在,输出流会自动创建文件。而输入流不会自动创建文件。

​ 2).输出流有flush()方法,输入流没有此方法。

图解

img

字节流

FileOutputStream

构造方法

​ 没有无参构造,必须要知道往哪里写

1
2
3
4
5
6
public FileOutputStream(File file) :创建一个向指定 File 对象表示的文件中写入数据的文件输出流。参数:file - 为了进行写入而打开的文件。、
public FileOutputStream(String name) :创建一个向具有指定名称的文件中写入数据的输出文件流。参数:name - 与系统有关的文件名 。
public FileOutputStream(File file, boolean append):创建一个向指定 File 对象表示的文件中写入数据的文件输出流。
                           如果第二个参数为 true,则将字节写入文件末尾处,而不是写入文件开始处。
public FileOutputStream(String name, boolean append):创建一个向具有指定 name 的文件中写入数据的输出文件流。
                             如果第二个参数为 true,则将字节写入文件末尾处,而不是写入文件开始处。

成员方法

1
2
3
4
public void write(int b): 将指定字节写入此文件输出流。参数:b - 要写入的字节。
public void write(byte[] b): 将 b.length 个字节从指定 byte 数组写入此文件输出流中。
public void write(byte[] b,int off,int len): 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此文件输出流。
public void close():关闭此文件输出流并释放与此流有关的所有系统资源。此文件输出流不能再用于写入字节。如果此流有一个与之关联的通道,则关闭该通道。

操作步骤

​ 1:创建字节输出流对象

​ 2:写数据,调用write()方法

​ 3:释放资源

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
/*
* 查看FileOutputStream的构造方法:
* FileOutputStream(File file)
* FileOutputStream(String name)// 看源码,其实本质是一样的。
*/
public class FileOutputStreamDemo {
public static void main(String[] args) throws IOException {
// 创建字节输出流对象
// FileOutputStream(File file)
// File file = new File("fos.txt");
// FileOutputStream fos = new FileOutputStream(file);

// FileOutputStream(String name)
FileOutputStream fos = new FileOutputStream("fos.txt");
// 实现数据的追加写入,用构造方法带第二个参数是true即可。
FileOutputStream fos2 = new FileOutputStream("fos.txt", true);
/*
创建字节输出流对象了做了几件事情:
A:调用系统功能去创建文件
B:创建fos对象
C:把fos对象指向这个文件
*/

//写数据
fos.write("hello,IO".getBytes());

fos.write(97); // 97 -- 底层二进制数据 -- 通过记事本打开 -- 找97对应的字符值 -- a
fos.write(57); // 57 -- 底层二进制数据 -- 通过记事本打开 -- 找57对应的字符值 -- 9
fos.write(55); // 55 -- 底层二进制数据 -- 通过记事本打开 -- 找55对应的字符值 -- 7

byte[] bys={97,98,99,100,101};
fos.write(bys);
fos.write(bys,1,3);

/*
实现数据的换行,不同的系统针对不同的换行符号识别是不一样的
windows:\r\n
linux:\n
Mac:\r
一些常见的高级记事本,是可以识别任意换行符号的。
*/
for (int x = 0; x < 10; x++) {
fos.write(("hello" + x).getBytes());
fos.write("\r\n".getBytes());
}

//释放资源
//关闭此文件输出流并释放与此流有关的所有系统资源。
fos.close();
/*
为什么一定要close()呢?
A:让流对象变成垃圾(fos),这样就可以被垃圾回收器回收了
B:通知系统去释放跟该文件相关的资源
*/
//java.io.IOException: Stream Closed
fos.write("java".getBytes());//因为流已关闭,所以写不进去。
}
}
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 FileOutputStreamDemo4 {
public static void main(String[] args) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream("z:\\fos.txt");// 报两个异常
// fos = new FileOutputStream("fos.txt");
fos.write("java".getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 如果fos不是null,才需要close()
// if (fos != null) {
try {
fos.close();// 为了保证close()一定会执行,就放到fianlly里。
} catch (IOException e) {
e.printStackTrace();
}
// }
}
}
}

FileInputStream

构造方法

​ 没有无参构造,必须要知道从哪里写

1
2
3
4
public FileInputStream(File file):通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定。
                   参数: file - 为了进行读取而打开的文件。
public FileInputStream(String name):通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定。
                     参数: name - 与系统有关的文件名。

成员方法

1
2
3
4
5
6
7
8
9
public int read():一次读一个字节,换行符号也能读到。读完后指针将指向下一个字节,有点类似迭代器的next()方法。
            从此输入流中读取一个数据字节。如果没有输入可用,则此方法将阻塞。
返回:下一个数据字节;如果已到达文件末尾,则返回 -1
public int read(byte[] b):一次读一个字节数组。
            从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。在某些输入可用之前,此方法将阻塞。
            返回:读入缓冲区的字节总数,如果因为已经到达文件末尾而没有更多的数据,则返回 -1
public int read(byte[] b, int off, int len):从此输入流中将最多 len 个字节的数据读入一个 byte 数组中。
            如果 len 不为 0,则在输入可用之前,该方法将阻塞;否则,不读取任何字节并返回 0
返回: 读入缓冲区的字节总数,如果因为已经到达文件末尾而没有更多的数据,则返回 -1

操作步骤

​ 1:创建字节输入流对象

​ 2:读数据,调用read()方法,并把数据显示到控制台

​ 3:释放资源

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 FileInputStreamDemo {
public static void main(String[] args) throws IOException {
// 创建字节输入流对象
FileInputStream fis = new FileInputStream("fis.txt");

// 方式一:一次读一个字节
int by = 0;
// 读取,赋值,判断
while ((by = fis.read()) != -1) {
System.out.print((char) by); // 输出到控制台,中文会乱码。但是,和输出流一起使用,复制中文文件是没问题的。
}

// 方式二:一次读一个字节数组(每次可以读取多个数据,提高了操作效率)
// 数组的长度一般是1024或者1024的整数倍
byte[] bys = new byte[1024]; // 1K 理论上来说,比方式一的效率高1024倍。
int len = 0;
while ((len = fis.read(bys)) != -1) {
// System.out.print(len);
// System.out.print(new String(bys));
System.out.print(new String(bys, 0, len)); // 输出到控制台,中文有可能乱码(当第1024个字节刚好是一个中文的时候)。和输出流一起使用,复制中文文件是没问题的。
}

// 释放资源
fis.close();
}
}

案例

使用2种方式实现

  A:复制文本文件

  B:复制图片

  C:复制视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 一次读一个字节,太慢了
public class CopyFileDemo {
public static void main(String[] args) throws IOException {
// 封装数据源
FileInputStream fis = new FileInputStream("a.txt");// java.io.FileNotFoundException: a.txt (系统找不到指定的文件。)
// 封装目的地
FileOutputStream fos = new FileOutputStream("b.txt");// 输出流会自动创建文件

int by = 0;
while ((by = fis.read()) != -1) {
fos.write(by);
}

// 释放资源(先关谁都行)
fos.close();
fis.close();
}
}

这一次复制中文确没有乱码,为什么?

上一次我们出现问题的原因在于我们每次获取到一个字节数据,就把该字节数据转换为了字符数据,然后输出到控制台。而现在,通过IO流读取数据,写到文本文件,你读取一个字节,我就写入一个字节,你没有做任何的转换。它会自己做转换,两个字节拼接成一个字符。看下面代码。

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 StringDemo {
public static void main(String[] args) {
String s1 = "abcde";
byte[] by1 = s1.getBytes();
System.out.println(Arrays.toString(by1));// [97, 98, 99, 100, 101]

String s2 = "我爱你中国";
byte[] by2;

try {
by2 = s2.getBytes("GBK");
System.out.println(Arrays.toString(by2));// [-50, -46, -80, -82, -60, -29, -42, -48, -71, -6]
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

char[] ch = s2.toCharArray();
System.out.println(ch);// 我爱你中国
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一次读一个字节数组,效率高
public class CopyFileDemo {
public static void main(String[] args) throws IOException {
// 封装数据源
FileInputStream fis = new FileInputStream("a.txt");
FileOutputStream fos = new FileOutputStream("b.txt");

// 复制数据
byte[] bys = new byte[1024];
int len = 0;
while ((len = fis.read(bys)) != -1) {
fos.write(bys, 0, len);
}

// 释放资源
fos.close();
fis.close();
}
}

案例B:复制图片,只能用字节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一次读一个字节,太慢了
public class CopyImageDemo {
public static void main(String[] args) throws IOException {
// 封装数据源
FileInputStream fis = new FileInputStream("a.jpg");
// 封装目的地
FileOutputStream fos = new FileOutputStream("b.jpg");

// 复制数据
int by = 0;
while ((by = fis.read()) != -1) {
fos.write(by);
}

// 释放资源
fos.close();
fis.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一次读一个字节数组,效率高
public class CopyFileDemo {
public static void main(String[] args) throws IOException {
// 封装数据源
FileInputStream fis = new FileInputStream("a.jpg");
FileOutputStream fos = new FileOutputStream("b.jpg");

// 复制数据
byte[] bys = new byte[1024];
int len = 0;
while ((len = fis.read(bys)) != -1) {
fos.write(bys, 0, len);
}

// 释放资源
fos.close();
fis.close();
}
}

案例C:复制视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一次读一个字节,太慢了
public class CopyMVDemo {
public static void main(String[] args) throws IOException {
// 封装数据源
FileInputStream fis = new FileInputStream("a.avi");
// 封装目的地
FileOutputStream fos = new FileOutputStream("b.avi");

// 复制数据
int by = 0;
while ((by = fis.read()) != -1) {
fos.write(by);
}

// 释放资源
fos.close();
fis.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 一次读一个字节数组,效率高
public class CopyMp4Demo {
public static void main(String[] args) throws IOException {
// 封装数据源
FileInputStream fis = new FileInputStream("a.avi");
// 封装目的地
FileOutputStream fos = new FileOutputStream("b.avi");

// 复制数据
byte[] bys = new byte[1024];
int len = 0;
while ((len = fis.read(bys)) != -1) {
fos.write(bys, 0, len);
}

// 释放资源
fos.close();
fis.close();
}
}

字节缓冲区流

  字节流一次读写一个数组的速度明显比一次读写一个字节的速度快很多,这是加入了数组这样的缓冲区效果,java本身在设计的时候,也考虑到了这样的设计思想(装饰设计模式),所以提供了字节缓冲区流(带缓冲区的字节类)这种类被称为:缓冲区类(高效类)

  构造方法可以指定缓冲区的大小,但是我们一般用不上,因为默认缓冲区大小就足够了。

  为什么不传递一个具体的文件或者文件路径,而是传递一个OutputStream对象呢?

    原因很简单,字节缓冲区流仅仅提供缓冲区,为高效而设计的。但是呢,真正的读写操作还得靠基本的流对象实现。

  A:BufferedOutputStream字节缓冲输出流,写入数据。该类实现缓冲的输出流。通过设置这种输出流,应用程序就可以将各个字节写入底层输出流中,而不必针对每次字节写入调用底层系统。

    构造方法:

      public BufferedOutputStream(OutputStream out):创建一个新的缓冲输出流,以将数据写入指定的底层输出流。

      public BufferedOutputStream(OutputStream out, int size):创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流。

  B:BufferedInputStream字节缓冲输入流,读取数据。在创建 BufferedInputStream 时,会创建一个内部缓冲区数组。(看API)

    构造方法:

      public BufferedInputStream(InputStream in):创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。创建一个内部缓冲区数组并将其存储在 buf 中。

      public BufferedInputStream(InputStream in, int size):创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。

                              创建一个长度为 size 的内部缓冲区数组并将其存储在 buf 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BufferedOutputStreamDemo {
public static void main(String[] args) throws IOException {
// FileOutputStream fos = new FileOutputStream("bos.txt");
// BufferedOutputStream bos = new BufferedOutputStream(fos);
// 简单写法
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));

// 写数据
bos.write("hello".getBytes());

// 释放资源
bos.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 注意:虽然我们有两种方式可以读取,但是,请注意,这两种方式针对同一个对象在一个代码中只能使用一个。因为read()方法类似迭代器的next()方法。读到末尾无法再读。
*/
public class BufferedInputStreamDemo {
public static void main(String[] args) throws IOException {
// BufferedInputStream(InputStream in)
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bos.txt"));

// 读取数据
// int by = 0;
// while ((by = bis.read()) != -1) {
// System.out.print((char) by);
// }

byte[] bys = new byte[1024];
int len = 0;
while ((len = bis.read(bys)) != -1) {
System.out.print(new String(bys, 0, len));
}

// 释放资源
bis.close();
}
}

案例

4种实现

  A:复制文本文件

  B:复制图片

  C:复制视频

案例C:

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
/*
* 需求:把G:\\a.avi复制到G:\\b.avi中
*
* 字节流四种方式复制文件:
* 基本字节流一次读写一个字节: 共耗时24199毫秒
* 基本字节流一次读写一个字节数组: 共耗时40毫秒
* 高效字节流一次读写一个字节: 共耗时200毫秒
* 高效字节流一次读写一个字节数组: 共耗时17毫秒
*/
public class CopyAVIDemo {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
method4("G:\\a.avi", "G:\\b.avi");
long end = System.currentTimeMillis();
System.out.println("共耗时" + (end - start) + "毫秒");
}

// 高效字节流一次读写一个字节数组,共耗时17毫秒
public static void method4(String string, String string2) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(string));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(string2, true));

byte[] bys = new byte[1024];
int len = 0;
while ((len = bis.read(bys)) != -1) {
bos.write(bys, 0, len);
}

bos.close();
bis.close();
}

// 高效字节流一次读写一个字节,共耗时200毫秒
public static void method3(String string, String string2) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(string));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(string2));

int by = 0;
while ((by = bis.read()) != -1) {
bos.write(by);
}

bos.close();
bis.close();
}

// 基本字节流一次读写一个字节数组,共耗时40毫秒
public static void method2(String string, String string2) throws IOException {
FileInputStream fis = new FileInputStream(string);
FileOutputStream fos = new FileOutputStream(string2);

byte[] bys = new byte[1024];
int len = 0;
while ((len = fis.read(bys)) != -1) {
fos.write(bys, 0, len);
}

fos.close();
fis.close();
}

// 基本字节流一次读写一个字节,共耗时24199毫秒
public static void method1(String string, String string2) throws IOException {
FileInputStream fis = new FileInputStream(string);
FileOutputStream fos = new FileOutputStream(string2);

int by = 0;
while ((by = fis.read()) != -1) {
fos.write(by);
}

fos.close();
fis.close();
}
}

字符流

介绍

​ 字节流操作中文数据不是特别的方便,所以就出现了转换流。转换流的作用就是把字节流转换字符流来使用。

​ 转换流其实是一个字符流。

字符流 = 字节流 + 编码表

编码表

就是由字符和对应的数值组成的一张表。

1、计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字,就将各个国家的文字用数字来表示,并一 一对应,形成一张表。

    ASCII:美国标准信息交换码。用一个字节的7位可以表示。

    ISO-8859-1:拉丁码表,欧洲码表。用一个字节的8位表示。

    GB2312:中国的中文编码表。

    GBK:中国的中文编码表升级,融合了更多的中文文字符号。

    GB18030:GBK的取代版本

    BIG-5码 :通行于台湾、香港地区的一个繁体字编码方案,俗称”大五码”。地区标准号为:CNS11643,这就是人们讲的BIG-5

    Unicode:国际标准码,融合了多种文字。所有文字都用两个字节来表示,Java语言使用的就是unicode

    UTF-8:最多用三个字节来表示一个字符。

    UTF-8不同,它定义了一种”区间规则”,这种规则可以和ASCII编码保持最大程度的兼容:

      它将Unicode编码为00000000-0000007F的字符,用单个字节来表示

      它将Unicode编码为00000080-000007FF的字符用两个字节表示

      它将Unicode编码为00000800-0000FFFF的字符用3字节表示

2、字符串中的编码问题

    编码:把能看懂的变成看不懂的

      String – byte[]:使用String的构造方法

      public byte[] getBytes(Charset charset):使用给定的 charset (字符集)将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。

    解码:把看不懂的变成能看懂的

      byte[] – String:使用String的成员方法

      public String(byte[] bytes, String charsetName):通过使用指定的 charset (字符集)解码指定的 byte 数组,构造一个新的 String。

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 StringDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
String s = "你好";

// String -- byte[]
byte[] bys1 = s.getBytes(); //使用默认字符集编码 [-60, -29, -70, -61]
byte[] bys2 = s.getBytes("GBK"); // [-60, -29, -70, -61]
byte[] bys3 = s.getBytes("UTF-8"); // [-28, -67, -96, -27, -91, -67]
System.out.println(Arrays.toString(bys1));
System.out.println(Arrays.toString(bys2));
System.out.println(Arrays.toString(bys3));

// byte[] -- String
String ss1 = new String(bys1); // 你好
String ss2 = new String(bys2, "GBK"); // 你好
String ss3 = new String(bys1, "UTF-8"); // ???
System.out.println(ss1);
System.out.println(ss2);
System.out.println(ss3);
}
}

IO流中的编码问题

​ 编码问题其实很简单,编码只要一致即可

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
public class OutputStreamWriterDemo {
public static void main(String[] args) throws IOException {
// 创建对象
OutputStreamWriter osw1 = new OutputStreamWriter(new FileOutputStream("osw.txt")); // 默认GBK
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("osw.txt"), "GBK"); // 指定GBK
OutputStreamWriter osw3 = new OutputStreamWriter(new FileOutputStream("osw.txt"), "UTF-8"); // 指定UTF-8

InputStreamReader isr1 = new InputStreamReader(new FileInputStream("osw.txt"));
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("osw.txt"), "GBK");
InputStreamReader isr3 = new InputStreamReader(new FileInputStream("osw.txt"), "UTF-8");
// 写数据
osw2.write("中国");// 父类的方法,写入字符串

// 释放资源
osw1.close();
osw2.close();
osw3.close();

// 读取数据,一次读取一个字符
int ch = 0;
while ((ch = isr3.read()) != -1) {
System.out.println(ch);
}

isr1.close();
isr2.close();
isr3.close();
}
}

OutputStreamWriter

字符流通向字节流的桥梁。按指定字符集写入字符

  构造方法:

    public OutputStreamWriter(OutputStream out):创建使用默认字符编码的 OutputStreamWriter。

    public OutputStreamWriter(OutputStream out, String charsetName):创建使用指定字符集的 OutputStreamWriter。指定编码。

  成员方法:

    public void write(int c):写入单个字符

    public void write(char[] cbuf):写入字符数组。是父类中的方法

    public void write(char[] cbuf, int off, int len):写入字符数组的某一部分。

    public void write(String str):写入字符串。是父类中的方法

    public void write(String str, int off, int len):写入字符串的某一部分。

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
/*
* close()和flush()的区别?
* A:close():先刷新一次缓冲区, 再关闭流对象,流对象不可以继续再使用了。
* B:flush():仅仅刷新缓冲区,流对象还可以继续使用。
*/
public class OutputStreamWriterDemo {
public static void main(String[] args) throws IOException {
// 创建对象
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("osw.txt", true));

// 写数据
osw.write('a');
osw.write(97);

char[] chs = { 'a', 'b', 'c', 'd', 'e' };
osw.write(chs);
osw.write(chs, 1, 3);

osw.write("我爱林青霞\r\n");
osw.write("我爱林青霞", 2, 3);

// 刷新缓冲区
osw.flush();

osw.write("我爱林青霞", 2, 3);

// 释放资源
osw.close();
// osw.write("我爱林青霞", 2, 3);// java.io.IOException: Stream closed
}
}

InputStreamReader

字节流通向字符流的桥梁。按照指定字符集读取字符

  构造方法:

    public InputStreamReader(InputStream in):创建一个使用默认字符集的 InputStreamReader。

    public InputStreamReader(InputStream in, String charsetName):创建使用指定字符集的 InputStreamReader。

  成员方法:

    public int read():读取单个字符。返回:读取的字符,如果已到达流的末尾,则返回 -1 。一次读取一个字符

    public int read(char[] cbuf):将字符读入数组。返回:读取的字符数,如果已到达流的末尾,则返回 -1。是父类中的方法。一次读取一个字符数组

    public int read(char[] cbuf, int offset, int length):将字符读入数组中的某一部分。返回:读取的字符数,如果已到达流的末尾,则返回 -1 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class InputStreamReaderDemo {
public static void main(String[] args) throws IOException {
// 创建对象
InputStreamReader isr = new InputStreamReader(new FileInputStream("osw.txt"));

// 一次读取一个字符
// int ch = 0;
// while ((ch = isr.read()) != -1) {
// System.out.print((char) ch);
// }

// 一次读取一个字符数组
char[] chs = new char[1024];
int len = 0;
while ((len = isr.read(chs)) != -1) {
System.out.print(new String(chs, 0, len));
}

// 释放资源
isr.close();
}
}

FileWriter和FileReader

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
/*
* 由于我们常见的操作都是使用本地默认编码,所以,不用指定编码。
* 而转换流的名称有点长,所以,Java就提供了其子类供我们使用。
* OutputStreamWriter = FileOutputStream + 编码表(GBK)
* FileWriter = FileOutputStream + 编码表(GBK)
*
* InputStreamReader = FileInputStream + 编码表(GBK)
* FileReader = FileInputStream + 编码表(GBK)
*/
public class CopyFileDemo {
public static void main(String[] args) throws IOException {
// 封装数据源
FileReader fr = new FileReader("a.txt");
// 封装目的地
FileWriter fw = new FileWriter("b.txt");

// 一次读取一个字符
// int ch = 0;
// while ((ch = fr.read()) != -1) {
// fw.write(ch);
// }

// 一次读取一个字符数组
char[] chs = new char[1024];
int len = 0;
while ((len = fr.read(chs)) != -1) {
fw.write(chs, 0, len);
fw.flush();
}

// 释放资源
fw.close();
fr.close();
}
}

字符缓冲流

  A: BufferedWriter:字符缓冲输出流。将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。 可以指定缓冲区的大小,或者接受默认的大小。在大多数情况下,默认值就足够大了。

    构造方法:

      public BufferedWriter(Writer out):创建一个使用默认大小输出缓冲区的缓冲字符输出流。

      public BufferedWriter(Writer out, int sz):创建一个使用给定大小输出缓冲区的新缓冲字符输出流。

    特殊方法:

      public void newLine():写入一个行分隔符。行分隔符字符串由系统属性 line.separator 定义,并且不一定是单个新行 (‘\n’) 符。

  B: BufferedReader:字符缓冲输入流。从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。

    构造方法:

      public BufferedReader(Reader in):创建一个使用默认大小输入缓冲区的缓冲字符输入流。

      public BufferedReader(Reader in, int sz):创建一个使用指定大小输入缓冲区的缓冲字符输入流。

    特殊方法:

      public String readLine():读取一个文本行。通过下列字符之一即可认为某行已终止:换行 (‘\n’)、回车 (‘\r’) 或回车后直接跟着换行。

                  返回:包含该行内容的字符串,不包含任何行终止符,如果已到达流末尾,则返回 null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BufferedWriterDemo {
public static void main(String[] args) throws IOException {
// BufferedWriter(Writer out)
// BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("bw.txt")));
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));

bw.write("hello");
bw.write("world");
bw.write("java");
bw.flush();

bw.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
// 创建字符缓冲输入流对象
// BufferedReader(Reader in)
BufferedReader br = new BufferedReader(new FileReader("bw.txt"));

// 方式1
int ch = 0;
while ((ch = br.read()) != -1) {
System.out.print((char) ch);
}

// 方式2
// char[] chs = new char[1024];
// int len = 0;
// while ((len = br.read(chs)) != -1) {
// System.out.print(new String(chs, 0, len));
// }

// 释放资源
br.close();
}
}
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
/*
* 字符缓冲流的特殊方法:
*/
public class BufferedDemo {
public static void main(String[] args) throws IOException {
// write();
read();
}

public static void read() throws IOException {
// 创建字符缓冲输入流对象
BufferedReader br = new BufferedReader(new FileReader("bw.txt"));

// public String readLine():一次读取一行数据
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);// 不包含任何行终止符,所以用println。
}

//释放资源
br.close();
}

public static void write() throws IOException {
// 创建字符缓冲输出流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));
for (int x = 0; x < 10; x++) {
bw.write("hello" + x);
// bw.write("\r\n");
bw.newLine();// 由系统属性决定换行符(行分隔符)
bw.flush();
}
bw.close();
}
}
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
/*
* 需求:把当前项目目录下的a.txt内容复制到当前项目目录下的b.txt中
*
* 数据源:
* a.txt -- 读取数据 -- 字符转换流 -- InputStreamReader -- FileReader -- BufferedReader
* 目的地:
* b.txt -- 写出数据 -- 字符转换流 -- OutputStreamWriter -- FileWriter -- BufferedWriter
*/
public class CopyFileDemo {
public static void main(String[] args) throws IOException {
// 封装数据源
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
// 封装目的地
BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));

// 两种方式其中的一种:一次读取一个字符数组
char[] chs = new char[1024];
int len = 0;
while ((len = br.read(chs)) != -1) {
bw.write(chs, 0, len);
bw.flush();
}

// 释放资源
bw.close();
br.close();
}
}
/************************************************************************************/
public class CopyFileDemo2 {
public static void main(String[] args) throws IOException {
// 封装数据源
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
// 封装目的地
BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));

// 读写数据
String line = null;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
bw.flush();
}

// 释放资源
bw.close();
br.close();
}
}

案例

复制文本文件(5种方式)

数据操作流

数据操作流(操作基本类型数据的流)(理解)

一、可以操作基本类型的数据
二、流对象名称
    DataInputStream
    DataOutputStream

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
/*
* 可以读写基本数据类型的数据
* 数据输入流:DataInputStream
* DataInputStream(InputStream in)
* 数据输出流:DataOutputStream
* DataOutputStream(OutputStream out)
*/
public class DataStreamDemo {
public static void main(String[] args) throws IOException {
// 写
// write();

// 读
read();
}

private static void read() throws IOException {
// DataInputStream(InputStream in)
// 创建数据输入流对象
DataInputStream dis = new DataInputStream(new FileInputStream("dos.txt"));

// 读数据
byte b = dis.readByte();
short s = dis.readShort();
int i = dis.readInt();
long l = dis.readLong();
float f = dis.readFloat();
double d = dis.readDouble();
char c = dis.readChar();
boolean bb = dis.readBoolean();

// 释放资源
dis.close();

System.out.println(b);
System.out.println(s);
System.out.println(i);
System.out.println(l);
System.out.println(f);
System.out.println(d);
System.out.println(c);
System.out.println(bb);
}

private static void write() throws IOException {
// DataOutputStream(OutputStream out)
// 创建数据输出流对象
DataOutputStream dos = new DataOutputStream(new FileOutputStream("dos.txt"));

// 写数据了
dos.writeByte(10);
dos.writeShort(100);
dos.writeInt(1000);
dos.writeLong(10000);
dos.writeFloat(12.34F);
dos.writeDouble(12.56);
dos.writeChar('a');
dos.writeBoolean(true);

// 释放资源
dos.close();
}
}

内存操作流

内存操作流

一、有些时候我们操作完毕后,未必需要产生一个文件,就可以使用内存操作流。例如:从数据库中取出二进制的文件,会用到 ByteArrayInputStream 。

二、三种

  A:ByteArrayInputStream,ByteArrayOutputStream

    ByteArrayInputStream:包含一个内部缓冲区,该缓冲区包含从流中读取的字节。内部计数器跟踪 read 方法要提供的下一个字节。

               关闭 ByteArrayInputStream 无效。此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException。

  B:CharArrayReader,CharArrayWriter

  C:StringReader,StringWriter

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
/*
* 内存操作流:用于处理临时存储信息的,程序结束,数据就从内存中消失。
* 字节数组:
* ByteArrayInputStream
* ByteArrayOutputStream
* 字符数组:
* CharArrayReader
* CharArrayWriter
* 字符串:
* StringReader
* StringWriter
*/
public class ByteArrayStreamDemo {
public static void main(String[] args) throws IOException {
// 写数据
// ByteArrayOutputStream()
ByteArrayOutputStream baos = new ByteArrayOutputStream();

// 写数据
for (int x = 0; x < 10; x++) {
baos.write(("hello" + x).getBytes());
}

// 释放资源
// 通过查看源码我们知道这里什么都没做,所以根本不需要close()
// baos.close();

// public byte[] toByteArray()
byte[] bys = baos.toByteArray();

// 读数据
// ByteArrayInputStream(byte[] buf)
ByteArrayInputStream bais = new ByteArrayInputStream(bys);

int by = 0;
while ((by = bais.read()) != -1) {
System.out.print((char) by);
}

// bais.close();
}
}

打印流(掌握)

一、字节打印流,字符打印流

二、特点:

  A:只操作目的地,不操作数据源

  B:可以操作任意类型的数据

  C:如果启用了自动刷新,在调用println()方法的时候,能够换行并刷新

  D:可以直接操作文件

    问题:哪些流可以直接操作文件呢?

    看API,如果其构造方法能够同时接收File和String类型的参数,一般都是可以直接操作文件的

三、复制文本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PrintWriterDemo {
public static void main(String args[]) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
PrintWriter pw = new PrintWriter(new FileWriter("b.txt"), true);

String line = null;
while ((line = br.readLine()) != null) {
pw.println(line);
}

pw.close();
br.close();
}
}
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
/*
* 打印流
* 字节流打印流 PrintStream
* 字符打印流 PrintWriter
*
* 打印流的特点:
* A:只有写数据的,没有读取数据。只能操作目的地,不能操作数据源。
* B:可以操作任意类型的数据。
* C:如果启动了自动刷新,能够自动刷新。
* D:该流是可以直接操作文本文件的。
* 哪些流对象是可以直接操作文本文件的呢?
* FileInputStream
* FileOutputStream
* FileReader
* FileWriter
* PrintStream
* PrintWriter
* 看API,查流对象的构造方法,如果同时有File类型和String类型的参数,一般来说就是可以直接操作文件的。
*
* 流:
* 基本流:就是能够直接读写文件的
* 高级流:在基本流基础上提供了一些其他的功能
*/
public class PrintWriterDemo {
public static void main(String[] args) throws IOException {
// 作为Writer的子类使用
PrintWriter pw = new PrintWriter("test.txt");

pw.write("hello");
pw.write("world");
pw.write("java");

pw.close();
}
}
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
/*
* 1:可以操作任意类型的数据。
* print()
* println()
* 2:启动自动刷新
* PrintWriter pw = new PrintWriter(new FileWriter("pw2.txt"), true);
* 还是应该调用println()的方法才可以
* 这个时候不仅仅自动刷新了,还实现了数据的换行。
*
* println()
* 其实等价于于:
* bw.write();
* bw.newLine();
* bw.flush();
*/
public class PrintWriterDemo2 {
public static void main(String[] args) throws IOException {
// 创建打印流对象
// PrintWriter pw = new PrintWriter("pw2.txt");
PrintWriter pw = new PrintWriter(new FileWriter("test.txt"), true);

// write()是搞不定的,怎么办呢?
// 我们就应该看看它的新方法
// pw.print(true);
// pw.print(100);
// pw.print("hello");

pw.println("hello");
pw.println(true);
pw.println(100);

pw.close();
}
}
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
/*
* 需求:DataStreamDemo.java复制到Copy.java中
* 数据源:
* DataStreamDemo.java -- 读取数据 -- FileReader -- BufferedReader
* 目的地:
* Copy.java -- 写出数据 -- FileWriter -- BufferedWriter -- PrintWriter
*/
public class CopyFileDemo {
public static void main(String[] args) throws IOException {
// 以前的版本
// 封装数据源
// BufferedReader br = new BufferedReader(new FileReader("DataStreamDemo.java"));
// // 封装目的地
// BufferedWriter bw = new BufferedWriter(new FileWriter("Copy.java"));
//
// String line = null;
// while ((line = br.readLine()) != null) {
// bw.write(line);
// bw.newLine();
// bw.flush();
// }
//
// bw.close();
// br.close();

// 打印流的改进版
// 封装数据源
BufferedReader br = new BufferedReader(new FileReader("DataStreamDemo.java"));
// 封装目的地
PrintWriter pw = new PrintWriter(new FileWriter("Copy.java"), true);

String line = null;
while ((line = br.readLine()) != null) {
pw.println(line);
}

pw.close();
br.close();
}
}

标准输入输出流

一、System类下面有这样的两个字段

​ in 标准输入流

​ out 标准输出流

二、三种键盘录入方式

  A:main方法的args接收参数

  B:System.in通过BufferedReader进行包装

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

  C:Scanner

    Scanner sc = new Scanner(System.in);

三、输出语句的原理和如何使用字符流输出数据

  A:原理

    System.out.println(“helloworld”);

    PrintStream ps = System.out;

    ps.println(“helloworld”);

  B:把System.out用字符缓冲流包装一下使用

    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.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
/*
* 标准输入输出流
* System类中的两个成员变量:
* public static final InputStream in “标准”输入流。
* public static final PrintStream out “标准”输出流。
*
* InputStream is = System.in;
* PrintStream ps = System.out;
*/
public class SystemOutDemo {
public static void main(String[] args) {
// 有这里的讲解我们就知道了,这个输出语句其本质是IO流操作,把数据输出到控制台。
System.out.println("helloworld");

// 获取标准输出流对象
PrintStream ps = System.out;
ps.println("helloworld");

ps.println();
// ps.print();//这个方法不存在

// System.out.println();
// System.out.print();
}
}
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
/*
* System.in 标准输入流。是从键盘获取数据的
*
* 键盘录入数据:
* A:main方法的args接收参数。
* java HelloWorld hello world java
* B:Scanner(JDK5以后的)
* Scanner sc = new Scanner(System.in);
* String s = sc.nextLine();
* int x = sc.nextInt()
* C:通过字符缓冲流包装标准输入流实现
* BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
*/
public class SystemInDemo {
public static void main(String[] args) throws IOException {
// //获取标准输入流
// InputStream is = System.in;// 一次读一个字节或一个字节数组
// //我要一次获取一行行不行呢?
// //行。
// //怎么实现呢?
// //要想实现,首先你得知道一次读取一行数据的方法是哪个呢?
// //readLine()
// //而这个方法在哪个类中呢?
// //BufferedReader
// //所以,你这次应该创建BufferedReader的对象,但是底层还是的使用标准输入流
// // BufferedReader br = new BufferedReader(is);
// //按照我们的推想,现在应该可以了,但是却报错了
// //原因是:字符缓冲流只能针对字符流操作,而你现在是字节流,所以不能是用?
// //那么,我还就想使用了,请大家给我一个解决方案?
// //把字节流转换为字符流,然后在通过字符缓冲流操作
// InputStreamReader isr = new InputStreamReader(is);
// BufferedReader br= new BufferedReader(isr);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

System.out.println("请输入一个字符串:");
String line = br.readLine();
System.out.println("你输入的字符串是:" + line);

System.out.println("请输入一个整数:");
// int i = Integer.parseInt(br.readLine());
line = br.readLine();
int i = Integer.parseInt(line);
System.out.println("你输入的整数是:" + i);

br.close();
}
}
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
/*
* 转换流的应用。
*/
public class SystemOutDemo2 {
public static void main(String[] args) throws IOException {
// 获取标准输入流
// // PrintStream ps = System.out;
// // OutputStream os = ps;
// OutputStream os = System.out; // 多态
// // 我能不能按照刚才使用标准输入流的方式一样把数据输出到控制台呢?
// OutputStreamWriter osw = new OutputStreamWriter(os);
// BufferedWriter bw = new BufferedWriter(osw);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

bw.write("hello");
bw.newLine();
// bw.flush();
bw.write("world");
bw.newLine();
// bw.flush();
bw.write("java");
bw.newLine();
bw.flush();

bw.close();
}
}

随机访问流

一、RandomAccessFile类,可以按照文件指针的位置读写数据。

  RandomAccessFile类既不是InputStream类的子类,也不是OutputStream类的子类。习惯上,仍然呈RandomAccessFile类创建的对象为一个流。RandomAccessFile流的指向既可以作为源也可以作为目的地。换句话说,当想对一个文件进行读写操作时,可以创建一个指向该文件的RandomAccessFile流,这样既可以从这个流读取文件的数据,也可以通过这个流向文件写入数据。

  RandomAccessFile类的两个构造方法:

    RandomAccessFile(String name, String mode):参数name用来确定一个文件名,给出创建的流的源(也是流的目的地)。参数mode取”r”(只读)或”rw”(可读写),决定创建的流对文件的操作权限。

    RandomAccessFile(File file, String mode):参数file是一个File对象,给出创建的流的源(也是流的目的地)。参数mode取”r”(只读)或”rw”(可读写),决定创建的流对文件的操作权限。创建对象时应捕获IOException异常。

  RandomAccessFile流对文件的读写方式更为灵活。因为以下两个方法:

    public void seek(long pos):设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作。偏移量的设置可能会超出文件末尾。偏移量的设置超出文件末尾不会改变文件的长度。只有在偏移量的设置超出文件末尾的情况下对文件进行写入才会更改其长度。 参数: pos - 从文件开头以字节为单位测量的偏移量位置,在该位置设置文件指针。

    public long getFilePointer():返回此文件中的当前偏移量。返回:到此文件开头的偏移量(以字节为单位),在该位置发生下一个读取或写入操作。

  RandomAccessFile类的常用方法有:

    public native long getFilePointer():获取当前流在文件中的读写位置。

    public native long length():获取文件的长度。

    public final byte readByte():从文件中读取一个字节。

    public final double readDouble():从文件中读取一个双精度浮点值(8个字节)。

    public final int readInt():从文件中读取一个int值(4个字节)。

    public final String readLine():从文件中读取一个文本行。

    public final String readUTF():从文件中读取一个UTF字符串。

    public void seek(long pos):定位当前流在文件中的读写的位置。

    public void write(byte b[]):写b.length个字节到文件。

    public final void writeDouble(double v):向文件写入一个双精度浮点值。

    public final void writeInt(int v):向文件写入一个int值。

    public final void writeUTF(String str):写入一个UTF字符串。

二、案例:

  A:写数据

  B:读数据

  C:获取和改变文件指针的位置

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
/*
* 随机访问流:
* RandomAccessFile类不属于流,是Object类的子类。
* 但它融合了InputStream和OutputStream的功能。
* 支持对文件的随机访问读取和写入。
*
* public RandomAccessFile(String name,String mode):第一个参数是文件路径,第二个参数是操作文件的模式。
* 模式有四种,我们最常用的一种叫"rw",这种方式表示我既可以写数据,也可以读取数据
*/
public class RandomAccessFileDemo {
public static void main(String[] args) throws IOException {
// write();
read();
}

private static void read() throws IOException {
// 创建随机访问流对象
RandomAccessFile raf = new RandomAccessFile("raf.txt", "rw");

int i = raf.readInt();
System.out.println(i);
// 该文件指针可以通过 getFilePointer方法读取,并通过 seek 方法设置。
System.out.println("当前文件的指针位置是:" + raf.getFilePointer());

char ch = raf.readChar();
System.out.println(ch);
System.out.println("当前文件的指针位置是:" + raf.getFilePointer());

String s = raf.readUTF();
System.out.println(s);
System.out.println("当前文件的指针位置是:" + raf.getFilePointer());

// 我不想重头开始了,我就要读取a,怎么办呢?
raf.seek(4);
ch = raf.readChar();
System.out.println(ch);
}

private static void write() throws IOException {
// 创建随机访问流对象
RandomAccessFile raf = new RandomAccessFile("raf.txt", "rw");

// 怎么玩呢?
raf.writeInt(100);
raf.writeChar('a');
raf.writeUTF("中国");

raf.close();
}
}

合并流

一、把多个输入流的数据写到一个输出流中。

二、构造方法:

  A:SequenceInputStream(InputStream s1, InputStream s2)

  B:SequenceInputStream(Enumeration<? extends InputStream> e)

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
/*
* 以前的操作:
* a.txt -- b.txt
* c.txt -- d.txt
*
* 现在想要:
* a.txt+b.txt -- c.txt
*/
public class SequenceInputStreamDemo {
public static void main(String[] args) throws IOException {
// SequenceInputStream(InputStream s1, InputStream s2)
// 需求:把ByteArrayStreamDemo.java和DataStreamDemo.java的内容复制到Copy.java中
InputStream s1 = new FileInputStream("a.txt");
InputStream s2 = new FileInputStream("b.txt");
SequenceInputStream sis = new SequenceInputStream(s1, s2);
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("c.txt"));

// 如何写读写呢,其实很简单,你就按照以前怎么读写,现在还是怎么读写
byte[] bys = new byte[1024];
int len = 0;
while ((len = sis.read(bys)) != -1) {
bos.write(bys, 0, len);
}

bos.close();
sis.close();
}
}
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
/*
* 以前的操作:
* a.txt -- b.txt
* c.txt -- d.txt
* e.txt -- f.txt
*
* 现在想要:
* a.txt+b.txt+c.txt -- d.txt
*/
public class SequenceInputStreamDemo2 {
public static void main(String[] args) throws IOException {
// 需求:把下面的三个文件的内容复制到Copy.java中
// ByteArrayStreamDemo.java,CopyFileDemo.java,DataStreamDemo.java

// SequenceInputStream(Enumeration e)
// 通过简单的回顾我们知道了Enumeration是Vector中的一个方法的返回值类型。
// Enumeration<E> elements()
Vector<InputStream> v = new Vector<InputStream>();
InputStream s1 = new FileInputStream("1.txt");
InputStream s2 = new FileInputStream("2.txt");
InputStream s3 = new FileInputStream("3.txt");
v.add(s1);
v.add(s2);
v.add(s3);
Enumeration<InputStream> en = v.elements();
SequenceInputStream sis = new SequenceInputStream(en);
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("4.txt"));

// 如何写读写呢,其实很简单,你就按照以前怎么读写,现在还是怎么读写
byte[] bys = new byte[1024];
int len = 0;
while ((len = sis.read(bys)) != -1) {
bos.write(bys, 0, len);
}

bos.close();
sis.close();
}
}

序列化流(理解)

一、可以把对象写入文本文件或者在网络中传输

二、如何实现序列化呢?

  让被序列化的对象所属类实现序列化接口。

  该接口是一个标记接口。没有功能需要实现。

三、注意问题:

  把数据写到文件后,在去修改类会产生一个问题。

  如何解决该问题呢?

    在类文件中,给出一个固定的序列化id值。

    而且,这样也可以解决黄色警告线问题

四、

​ 什么时候序列化?

​ 如何实现序列化?

​ 什么是反序列化?

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
/*
* 序列化流:把对象按照流一样的方式存入文本文件或者在网络中传输。对象 -- 流数据(ObjectOutputStream)
* 反序列化流:把文本文件中的流对象数据或者网络中的流对象数据还原成对象。流数据 -- 对象(ObjectInputStream)
*/
public class ObjectStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 由于我们要对对象进行序列化,所以我们先自定义一个类
// 序列化数据其实就是把对象写到文本文件
// write();

read();
}

private static void read() throws IOException, ClassNotFoundException {
// 创建反序列化对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("oos.txt"));

// 还原对象
Object obj = ois.readObject();

// 释放资源
ois.close();

// 输出对象
System.out.println(obj);
}

private static void write() throws IOException {
// 创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("oos.txt"));

// 创建对象
Person p = new Person("林青霞", 27);

// public final void writeObject(Object obj)
oos.writeObject(p);

// 释放资源
oos.close();
}
}
/*
* NotSerializableException:未序列化异常
*
* 类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。
* 该接口居然没有任何方法,类似于这种没有方法的接口被称为标记接口。
*
* 修改java文件后报错如下:
* java.io.InvalidClassException: Person; local class incompatible:stream classdesc serialVersionUID = -2071565876962058344, local class serialVersionUID = -8345153069362641443
* java.io.InvalidClassException: Person; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = -2071565876962058344
*
* 为什么会有问题呢?
* Person类实现了序列化接口,那么它本身也应该有一个标记值。
* 这个标记值假设是100。
* 开始的时候:
* Person.class -- id=100
* wirte数据: oos.txt -- id=100
* read数据: oos.txt -- id=100
*
* 现在:
* Person.class -- id=200
* wirte数据: oos.txt -- id=100
* read数据: oos.txt -- id=100
* 我们在实际开发中,可能还需要使用以前写过的数据,不能重新写入。怎么办呢?
* 回想一下原因是因为它们的id值不匹配。
* 每次修改java文件的内容的时候,class文件的id值都会发生改变。
* 而读取文件的时候,会和class文件中的id值进行匹配。所以,就会出问题。
* 但是呢,如果我有办法,让这个id值在java文件中是一个固定的值,这样,你修改文件的时候,这个id值还会发生改变吗?
* 不会。现在的关键是我如何能够知道这个id值如何表示的呢?
* 不用担心,你不用记住,也没关系,点击鼠标即可。
* 你难道没有看到黄色警告线吗?
*
* 我们要知道的是:
* 看到类实现了序列化接口的时候,要想解决黄色警告线问题,就可以自动产生一个序列化id值。
* 而且产生这个值以后,我们对类进行任何改动,它读取以前的数据是没有问题的。
*
* 注意:
* 我一个类中可能有很多的成员变量,有些我不想进行序列化。请问该怎么办呢?
* 使用transient关键字声明不需要序列化的成员变量
*/
class Person implements Serializable {

private static final long serialVersionUID = -2071565876962058344L;

private String name;

// private int age;

private transient int age;

// int age;

public Person() {
}

public Person(String name, int age) {
super();
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;
}

@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}

Properties

一、Properties是一个集合类,Hashtable的子类

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
/*
* Properties:属性集合类。是一个可以和IO流相结合使用的集合类。
* Properties 可保存在流中或从流中加载。属性列表中每个键及其对应值都是一个字符串。
*
* 是Hashtable的子类,说明是一个Map集合。
*/
public class PropertiesDemo {
public static void main(String[] args) {
// 作为Map集合的使用
// 下面这种用法是错误的,一定要看API,如果没有<>,就说明该类不是一个泛型类,在使用的时候就不能加泛型
// Properties<String, String> prop = new Properties<String, String>();

Properties prop = new Properties();

// 添加元素
prop.put("it002", "hello");
prop.put("it001", "world");
prop.put("it003", "java");

// System.out.println("prop:" + prop);

// 遍历集合
Set<Object> set = prop.keySet();
for (Object key : set) {
Object value = prop.get(key);
System.out.println(key + "---" + value);
}
}
}

二、特有功能

  A:public Object setProperty(String key,String value):添加元素

  B:public String getProperty(String key):获取元素

  C:public Set<String> stringPropertyNames():获取所有键的集合

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 class PropertiesDemo2 {
public static void main(String[] args) {
// 创建集合对象
Properties prop = new Properties();

// 添加元素
// prop.put("11", "11");
prop.setProperty("张三", "30");
prop.setProperty("李四", "40");
prop.setProperty("王五", "50");

// public Set<String> stringPropertyNames():获取所有的键的集合
Set<String> set = prop.stringPropertyNames();
for (String key : set) {
// String value = prop.getProperty(key);
String value = (String)prop.get(key);
System.out.println(key + "---" + value);
}
}
}


/*
class Hashtalbe<K,V> {
public V put(K key,V value) { ... }
}
class Properties extends Hashtable {
public V setProperty(String key, Stringvalue) {
return put(key,value);
}
}
*/

三、和IO流结合的方法

  把键值对形式的文本文件内容加载到集合中,(把文件中的数据读取到集合中)

  public void load(Reader reader)

  public void load(InputStream inStream)

  把集合中的数据存储到文本文件中

  public void store(Writer writer,String comments)

  public void store(OutputStream out,String comments)

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
/*
* 这里的集合必须是Properties集合:
* public void load(Reader reader):把文件中的数据读取到集合中
* public void store(Writer writer,String comments):把集合中的数据存储到文件
*
* 单机版游戏:
* 进度保存和加载。
* 三国群英传,三国志,仙剑奇侠传...
*
* 吕布=1
* 方天画戟=1
*/
public class PropertiesDemo3 {
public static void main(String[] args) throws IOException {
// myLoad();

myStore();
}

private static void myStore() throws IOException {
// 创建集合对象
Properties prop = new Properties();

prop.setProperty("林青霞", "27");
prop.setProperty("武鑫", "30");
prop.setProperty("刘晓曲", "18");

// public void store(Writer writer,String comments):把集合中的数据存储到文件
Writer w = new FileWriter("name.txt");
prop.store(w, "comments – 属性列表的描述");
w.close();
}

private static void myLoad() throws IOException {
Properties prop = new Properties();

// public void load(Reader reader):把文件中的数据读取到集合中
// 注意:这个文件的数据必须是键值对形式
Reader r = new FileReader("name.txt");
prop.load(r);
r.close();

System.out.println("prop:" + prop);
}
}

四、案例:

  A:根据给定的文件判断是否有键为”lisi”的,如果有就修改其值为100

  B:写一个程序实现控制猜数字小游戏程序不能玩超过5次

案例A:

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
/*
* 我有一个文本文件(user.txt),我知道数据是键值对形式的,但是不知道内容是什么。
* 请写一个程序判断是否有“lisi”这样的键存在,如果有就改变其实为”100”
*
* 分析:
* A:把文件中的数据加载到集合中
* B:遍历集合,获取得到每一个键
* C:判断键是否有为"lisi"的,如果有就修改其值为"100"
* D:把集合中的数据重新存储到文件中
*/
public class PropertiesTest {
public static void main(String[] args) throws IOException {
// 把文件中的数据加载到集合中
Properties prop = new Properties();
Reader r = new FileReader("user.txt");
prop.load(r);
r.close();

// 遍历集合,获取得到每一个键
Set<String> set = prop.stringPropertyNames();
for (String key : set) {
// 判断键是否有为"lisi"的,如果有就修改其值为"100"
if ("lisi".equals(key)) {
prop.setProperty(key, "100");
break;
}
}

// 把集合中的数据重新存储到文件中
Writer w = new FileWriter("user.txt");
prop.store(w, null);
w.close();
}
}

案例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
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
/*
* 我有一个猜数字小游戏的程序,请写一个程序实现在测试类中只能用5次,超过5次提示:游戏试玩已结束,请付费。
*/
public class PropertiesTest2 {
public static void main(String[] args) throws IOException {
// 读取某个地方的数据,如果次数不大于5,可以继续玩。否则就提示"游戏试玩已结束,请付费。"
// 创建一个文件
// File file = new File("count.txt");
// if (!file.exists()) {
// file.createNewFile();
// }

// 把数据加载到集合中
Properties prop = new Properties();
Reader r = new FileReader("count.txt");
prop.load(r);
r.close();

// 我自己的程序,我当然知道里面的键是谁
String value = prop.getProperty("count");
int number = Integer.parseInt(value);

if (number > 5) {
System.out.println("游戏试玩已结束,请付费。");
System.exit(0);
} else {
number++;
prop.setProperty("count", String.valueOf(number));
Writer w = new FileWriter("count.txt");
prop.store(w, null);
w.close();

GuessNumber.start();
}
}
}

/**
* 这是猜数字小游戏
*/
class GuessNumber {
private GuessNumber() {
}

public static void start() {
// 产生一个随机数
int number = (int) (Math.random() * 100) + 1;

// 定义一个统计变量
int count = 0;

while (true) {
// 键盘录入一个数据
Scanner sc = new Scanner(System.in);
System.out.println("请输入数据(1-100):");
int guessNumber = sc.nextInt();

count++;

// 判断
if (guessNumber > number) {
System.out.println("你猜的数据" + guessNumber + "大了");
} else if (guessNumber < number) {
System.out.println("你猜的数据" + guessNumber + "小了");
} else {
System.out.println("恭喜你," + count + "次就猜中了");
break;
}
}
}
}

NIO

一、JDK4出现NIO。

  新IO和传统的IO有相同的目的,都是用于进行输入输出的,但新IO使用了不同的方式来处理输入输出,采用内存映射文件的方式,将文件或者文件的一段区域映射到内存中,就可以像访问内存一样的来访问文件了,这种方式效率比旧IO要高很多,但是目前好多地方我们看到的还是旧IO的引用。

  JDK4新IO要了解的类(看API)

  Buffer(缓冲),Channer(通道)

二、JDK7的NIO的使用

  Path:与平台无关的路径。

  Paths:包含了返回Path的静态方法。

    public static Path get(URI uri):根据给定的URI来确定文件路径。

  Files:操作文件的工具类。提供了大量的方法,简单了解如下方法

    public static long copy(Path source, OutputStream out) :复制文件

    public static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption… options):把集合的数据写到文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class NIODemo {
public static void main(String[] args) throws IOException {
//复制文件
// public static long copy(Path source,OutputStream out)
Files.copy(Paths.get("a.txt"), new FileOutputStream("Copy.txt"));

//把集合中的数据写到文件
ArrayList<String> array = new ArrayList<String>();
array.add("hello");
array.add("world");
array.add("java");
Files.write(Paths.get("array.txt"), array, Charset.forName("GBK"));
}
}
0%