函数式编程

新日期API

Java有两套日期和时间的API:

  1. 旧的Date、Calendar和TimeZone;(java.util包)
  2. java8后,新的LocalDateTime、ZonedDateTime、ZoneId等。(java.time包)

Instant(时刻) & Duration(时间间隔)

  1. 创建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
TRFunction<T, R>
voidTSupplier<T,>
TvoidConsumer<T,>
voidvoidRunnable
TBooleanPredicate<T,>
TTUnaryOperator<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的特点是:

  1. Stream API提供了一套新的流式处理的抽象序列;
  2. Stream API支持函数式编程和链式操作;
  3. 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包也提供了对应的并发集合类。多线程同时读写并发集合是安全的;

接口线程不安全线程安全
ListArrayListCopyOnWriteArrayList
MapHashMapConcurrentHashMap
SetHashSet / TreeSetCopyOnWriteArraySet
QueueArrayDeque / LinkedListArrayBlockingQueue / LinkedBlockingQueue
DequeArrayDeque / LinkedListLinkedBlockingDeque

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提供的原子操作可以简化多线程编程:

  • 原子操作实现了无锁的线程安全;
  • 适用于计数器,累加器等。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

抽抽了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值