Java8新特性、函数式编程
新日期API
Java有两套日期和时间的API:
- 旧的Date、Calendar和TimeZone;(java.util包)
- java8后,新的LocalDateTime、ZonedDateTime、ZoneId等。(java.time包)
Instant(时刻) & Duration(时间间隔)
- 创建Instant实例
// 获取的是默认UTC时区的时间,2021-02-22T01:25:29.560Z
Instant start = Instant.now();
// 获取北京时间(增加8个小时),2021-02-22T09:25:29.591Z
Instant now = Instant.now().plusMillis(TimeUnit.HOURS.toMillis(8));
// 源码
public static Instant now() {
return Clock.systemUTC().instant();
}
- 获取时间戳
System.out.println("秒数:" + now.getEpochSecond()); // 秒数:1613985880
System.out.println("毫秒数:" + now.toEpochMilli()); // 毫秒数:1613985880633
- 计算时间差
Instant start = Instant.now();
Thread.sleep(2000);
Instant end = Instant.now();
Duration duration = Duration.between(start, end);
System.out.println("时间差:" + duration.getSeconds()); // 时间差:2
ZonedDateTime(带时区的日期和时间) & LocalDateTime(本地日期和时间)
- LocalDateTime
本地日期和时间通过now()获取到的总是以当前默认时区返回的,和旧API不同,LocalDateTime、LocalDate和LocalTime默认严格按照ISO 8601规定的日期和时间格式进行打印
LocalDateTime now = LocalDateTime.now();
System.out.println(now); // 2021-02-22T09:44:02.749
LocalDateTime dt2 = LocalDateTime.of(2019, 11, 30, 15, 16, 17);
System.out.println(dt2); // 2019-11-30T15:16:17
LocalDateTime dt = LocalDateTime.parse("2019-11-19T15:16:17");
System.out.println(dt); // 2019-11-19T15:16:17
- ZoneDateTime
Instant dateTimeInstant = Instant.now();
ZonedDateTime dtUtc = ZonedDateTime.ofInstant(dateTimeInstant, ZoneId.of("UTC"));
System.out.println(dtUtc); // 2021-02-22T02:01:07.008Z[UTC]
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // 2021-02-22T10:01:07.030+08:00[Asia/Shanghai]
ZonedDateTime now1 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println(now1); // 2021-02-22T10:01:07.030+08:00[Asia/Shanghai]
LocalDateTime localDateTime = LocalDateTime.now();
ZonedDateTime now2 = localDateTime.atZone(ZoneId.of("Asia/Shanghai"));
System.out.println(now2); // 2021-02-22T10:01:07.030+08:00[Asia/Shanghai]
DateTimeFormatter(取代SimpleDateFormat)
格式化字符串的使用方式与SimpleDateFormat完全一致。
DateTimeFormatter f= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
DateTimeFormatter f1= DateTimeFormatter.ofPattern("yyyy-MMMM-dd HH:mm", Locale.US);
Instant start = Instant.now();
ZonedDateTime dtSH = start.atZone(ZoneId.systemDefault());
System.out.println(dtSH); // 2021-02-22T10:10:21.251+08:00[Asia/Shanghai]
DateTimeFormatter dtf= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String dateStr = dtf.format(dtSH);
System.out.println(dateStr); // 2021-02-22 10:10:21
TemporalAdjusters
Instant start = Instant.now();
ZonedDateTime dtSH = start.atZone(ZoneId.of("Asia/Shanghai"));
ZonedDateTime with = dtSH.with(TemporalAdjusters.next(DayOfWeek.THURSDAY));
System.out.println(with);
函数式编程
Lambda表达式
我们经常把支持函数式编程的编码风格称为Lambda表达式。
public static Function<Integer, String> function = (Integer a) -> {return String.valueOf(a);};
// 只有一个表达式,可省略大括号
public static Function<Integer, String> function1 = (Integer a) -> String.valueOf(a);
// 参数类型可推导,可省略
public static Function<Integer, String> function2 = (a) -> String.valueOf(a);
// 类型可推导且只有一个参数,可省略参数括号
public static Function<Integer, String> function3 = a -> String.valueOf(a);
函数式接口
参数 | 返回值 | class |
---|---|---|
T | R | Function<T, R> |
void | T | Supplier<T,> |
T | void | Consumer<T,> |
void | void | Runnable |
T | Boolean | Predicate<T,> |
T | T | UnaryOperator<T,> |
方法引用
public class LambdaBasic {
public LambdaBasic() {}
public LambdaBasic(String v) {}
public String twoint2String(int x, int y) {
return x + " " + y;
}
public static String int2StringFn(Integer i) {
return String.valueOf(i);
}
private static Function<Integer, Function<Integer, String>> twoint2String = x -> y -> x + " " + y;
/** 方法引用 */
private Function<Integer, String> staticRef = LambdaBasic::int2StringFn;
/** 无参构造函数引用 */
private static Supplier<LambdaBasic> constructor = LambdaBasic::new;
/** 有参构造函数引用 */
private static Function<String, LambdaBasic> constructor1 = LambdaBasic::new;
/** 引用静态方法 */
private BiFunction<Integer, Integer, String> instanceRef = this::twoint2String;
public String letLBDo(TriFunction<LambdaBasic, Integer, Integer, String> cb, int i1, int i2) {
return cb.apply(this, i1, i2);
}
public static void main(String[] args) {
LambdaBasic lb = constructor.get();//new LambdaBasic ()
// LambdaBasic::twoint2String有两个参数,但是letLBDo的cb有三个参数,其实隐含的第一个参数是this,和String的compareTo方法类似。
System.out.println(lb.letLBDo(LambdaBasic::twoint2String, 2, 3));
}
}
Stream
Stream API的特点是:
- Stream API提供了一套新的流式处理的抽象序列;
- Stream API支持函数式编程和链式操作;
- Stream可以表示无限序列,并且大多数情况下是惰性求值的。
Stream的创建
- Stream.of()
Stream<String> stream = Stream.of("A", "B", "C", "D");
// forEach()方法相当于内部循环调用,
// 可传入符合Consumer接口的void accept(T t)的方法引用:
stream.forEach(System.out::println);
- 基于数组或Collection
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
stream1.forEach(System.out::println);
List<String> list = new ArrayList<>();
list.add("X");
list.add("Y");
list.add("z");
Stream<String> stream2 = list.stream();
stream2.forEach(System.out::println);
- 基于Supplier
class NatualSupplier implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
Stream<Integer> natual = Stream.generate(new NatualSupplier());
// 注意:无限序列必须先变成有限序列再打印:
natual.limit(20).forEach(System.out::println);
- 基本类型
// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
使用map
所谓map操作,就是把一种操作运算,映射到一个序列的每一个元素上。
例如,对x计算它的平方,可以使用函数f(x) = x * x。我们把这个函数映射到一个序列1,2,3,4,5上,就得到了另一个序列1,4,9,16,25。
可见,map操作,把一个Stream的每个元素一一对应到应用了目标函数的结果上。
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);
map()方法接收的对象是Function接口对象,它定义了一个apply()方法,负责把一个T类型转换成R类型。
总结:
- map()方法用于将一个Stream的每个元素映射成另一个元素并转换成一个新的Stream;
- 可以将一种元素类型转换成另一种元素类型。
使用filter
所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream。
例如,我们对1,2,3,4,5这个Stream调用filter(),传入的测试函数f(x) = x % 2 != 0用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列1,3,5。
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(n -> n % 2 != 0)
.forEach(System.out::println);
filter()方法接收的对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件。
总结:
- 使用filter()方法可以对一个Stream的每个元素进行测试,通过测试的元素被过滤后生成一个新的Stream。
使用reduce
map()和filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
System.out.println(sum); // 45
reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果。
总结:
- reduce()方法将一个Stream的每个元素依次作用于BinaryOperator,并将结果合并。
- reduce()是聚合方法,聚合方法会立刻对Stream进行计算。
使用collect
private static Map<Integer, Programmer> toMap(List<Programmer> programmers){
Map<Integer, Programmer> ret = programmers.stream()
.collect(Collectors.toMap(Programmer::getLevel, p->p, (p1,p2) -> p2, HashMap::new));
return ret;
}
public static Map<Integer, Map<Integer, List<Programmer>>> groupBy(List<Programmer> programmers) {
return programmers.stream().collect(Collectors.groupingBy(
Programmer::getLevel,
Collectors.groupingBy(Programmer::getSalary)
));
}
Concurrent
使用wait和notify
在Java程序中,synchronized解决了多线程竞争的问题。
class TaskQueue {
Queue<String> queue = new LinkedList<>();
/*
* 当一个线程在this.wait()等待时,它就会释放this锁,
* 从而使得其他线程能够在addTask()方法获得this锁。
*/
public synchronized void addTask(String s) {
this.queue.add(s);
/*
* 线程立刻对this锁对象调用notifyAll()方法,
* 这个方法会唤醒所有正在this锁等待的线程
* (就是在getTask()中位于this.wait()的线程),
* 从而使得等待线程从this.wait()方法返回。
*/
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
/*
* 多个线程被唤醒后,只有一个线程能获取this锁,
* 此刻,该线程执行queue.remove()可以获取到队列的元素,
* 然而,剩下的线程如果获取this锁后执行queue.remove(),
* 此刻队列可能已经没有任何元素了,
* 所以,要始终在while循环中wait(),而不是if,并且每次被唤醒后拿到this锁就必须再次判断:
*/
while (queue.isEmpty()) {
/*
* 当一个线程执行到getTask()方法内部的while循环时,
* 它必定已经获取到了this锁,此时,线程执行while条件判断,
* 如果条件成立(队列为空),线程将执行this.wait(),进入等待状态。
*/
// 这里的关键是:wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()。
// 释放this锁:
this.wait();
// wait()返回后,重新获取this锁
}
return queue.remove();
}
}
使用ReentrantLock
从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。
java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁:
/**
* 传统的synchronized代码
*/
public class Counter {
private int count;
public void add(int n) {
synchronized(this) {
count += n;
}
}
}
/**
* 用ReentrantLock替代
*/
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n) {
lock.lock();
try {
count += n;
} finally {
lock.unlock();
}
}
}
ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁。
lock.tryLock(1, TimeUnit.SECONDS)可以尝试获取锁。
使用Condition
Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的。
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
// 2. signalAll()会唤醒所有等待线程;
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
// 1. await()会释放当前锁,进入等待状态;
condition.await();
// 3. 唤醒线程从await()返回后需要重新获得锁。
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
condition.await(1, TimeUnit.SECOND);可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来
使用ReadWriteLock
使用ReadWriteLock可以解决这个问题,它保证:
- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)。
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}
使用StampedLock
Java 8引入了新的读写锁:StampedLock。(读的过程中也允许获取写锁后写入)
public class Point {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。
可见,StampedLock把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:
- 一是代码更加复杂,
- 二是StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。
使用Concurrent集合
java.util.concurrent包也提供了对应的并发集合类。多线程同时读写并发集合是安全的;
接口 | 线程不安全 | 线程安全 |
---|---|---|
List | ArrayList | CopyOnWriteArrayList |
Map | HashMap | ConcurrentHashMap |
Set | HashSet / TreeSet | CopyOnWriteArraySet |
Queue | ArrayDeque / LinkedList | ArrayBlockingQueue / LinkedBlockingQueue |
Deque | ArrayDeque / LinkedList | LinkedBlockingDeque |
Atomic类
Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。
/*
* CAS是指,在这个操作中,如果AtomicInteger的当前值是prev,那么就更新为next,返回true。
* 如果AtomicInteger的当前值不是prev,就什么也不干,返回false
*/
public int incrementAndGet(AtomicInteger var) {
int prev, next;
do {
prev = var.get();
next = prev + 1;
} while ( ! var.compareAndSet(prev, next));
return next;
}
使用java.util.concurrent.atomic提供的原子操作可以简化多线程编程:
- 原子操作实现了无锁的线程安全;
- 适用于计数器,累加器等。