更改JAVA版本
https://mkyong.com/java/how-to-install-java-on-mac-osx/
Java 注解
@interface
用于定义注解, 注意,注解的参数类似于无参数方法。 可以用default设定一个默认值。最常用的参数名是value
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
元注解
@Target
用来定义注解可以被用在源码中的哪些位置
- ElementType.TYPE: 用在类或者接口上
- ElementType.FIELD: 用在字段上
- ElementType.METHOD:用在方法上
- ElementType.CONSTRUCTOR:用在构造方法上
- ElementType.PARAMETER: 用于注解方法参数
Target 注解的参数可以是一个数组。 如果只有一个注解参数,则去掉{} 就可以。
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}
@Retention
Retention 定义了注解的生命周期。
- RetentionPolicy.SOURCE: 仅编译器
- RetentionPolicy.CLASS: 仅class 文件
- RetentionPolicy.RUNTIME: 运行期
注意: Retention 默认为Class 级别。 而我们定义的注解主要作用在运行期。 所以务必要申明:
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Repeatable
Repeatable 用于定义这个注解是否是可以重复的,具体的作用如下:
@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Target(ElementType.TYPE)
public @interface Reports {
Report[] value();
}
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
@Inherited
定义被这个注解修饰的类的子类是否可以继承父类的注解。 仅针对@Target(ElementType.TYPE) 类型的注解有效且仅针对class 继承, 对interface 继承无效,
定义注解的步骤
- 用@interface 定义注解
- 添加参数,默认值
- 用元注解配置注解。
处理注解
SOURCE 类型的注解会在编译期被丢掉。 CLASS 类型的注解会被保存在CLASS 文件中,他们不会被加载进JVM。 RUNTIME 类型的注解会被加载进JVM,读取注解
判断注解是否存在于Class, Field, Method 或者 Constructor。
- Class.isAnnotationPresent(Class)
- Field.isAnnotationPresent(Class)
- Method.isAnnotationPresent(Class)
- Constructor.isAnnotationPresent(Class)
定义注解本身对程序没有影响。 必须要编写注解检查逻辑。 例如Junit 会自动运行@Test 标记的测试方法。 检查自定义实例:
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
// 遍历所有Field:
for (Field field : person.getClass().getFields()) {
// 获取Field定义的@Range:
Range range = field.getAnnotation(Range.class);
// 如果@Range存在:
if (range != null) {
// 获取Field的值:
Object value = field.get(person);
// 如果值是String:
if (value instanceof String) {
String s = (String) value;
// 判断值是否满足@Range的min/max:
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}
注意:方法参数本身可以看作一个数组,而每个参数又可以被多个注解定义, 一次性获取所有注解就必须用二维数组来表示:
public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}
// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range) { // @Range注解
Range r = (Range) anno;
}
if (anno instanceof NotNull) { // @NotNull注解
NotNull n = (NotNull) anno;
}
}
泛型
泛型是一个代码模版,可以套用多种类型
public class ArrayList<T> {
private T[] array;
private int size;
public void add(T e) {...}
public void remove(int index) {...}
public T get(int index) {...}
}
// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();
向上转型
public class ArrayList<T> implements List<T> {
...
}
List<String> list = new ArrayList<String>();
使用泛型
类型推断
// 可以省略后面的Number,编译器可以自动推断泛型类型:
List<Number> list = new ArrayList<>();
泛型接口
例如Arrays.sort(Object[]) 可以对任意类型数组排序, 但待排序元素必须实现 Comparable.
public interface Comparable<T> {
/**
* 返回负数: 当前实例比参数o小
* 返回0: 当前实例与参数o相等
* 返回正数: 当前实例比参数o大
*/
int compareTo(T o);
}
编写泛型
- 编写泛型需要定义泛型类型
- 静态方法不能引用泛型类型
, 需要用其他方法 等
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... }
// 静态泛型方法应该使用其他类型区分:
public static <K> Pair<K> create(K first, K last) {
return new Pair<K>(first, last);
}
}
擦拭法
擦拭法指虚拟机对泛型一无所知,一切由编译器完成。 编译器首先将所有泛型视为Object。 再根据类型
- 不能是基本类型如int
- 不能获取带泛型类的Class, 如Pair
.class - 不能判断带泛型类的类型:x instanceof Pair
- 不能实例化T类型: new T()
-
Extends 通配符
使用<? extends Number> 的泛型定义称为上界通配符
方法内部可以调用获取Number 引用的方法, 例如:Number n = obj.getFirst();
- 方法内部无法调用传入Number 引用的方法, 例如:obj.setFirst(Number n);
-
Super 通配符
Stream
常用的stream 操作
转换操作: map, filter, sorted, distincted
- 合并操作: concat, flatMap
- 并行操作: parallel
- 聚合操作:reduce, collect, count, max, min, sum, average
- 其他操作: allMatch, anyMatch, forEach
反射
集合
Collection
除map 外所有集合的根接口。 Java 的util 包主要提供了以下集合List, Set, Map。List
顺序访问的长度可变的有序表。优先使用ArrayList 而不是 LinkedList
注意:在list 中查找元素时(contains 方法), List 的实现类通过equals 方法判断两个元素是否相等, 因此,放入的元素必须正确复写equals 方法。 如果不需要查找逻辑可以不覆写。Map
遍历map :
注意: Map 使用equals 判断key 是否相等,使用自定义的key 必须重写equals 方法。 HashMap 计算每个key 的hash code 用来定位索引。equals 用到的每一个字段都必须用来计算hashcode。public class Main { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); map.put("apple", 123); map.put("pear", 456); map.put("banana", 789); for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(key + " = " + value); } } }
EnumMap
EnumMap 专门为枚举类设计, key 是枚举类。因为枚举类使用单例模式,所以不需要计算hashcode 也可以定位到value。public class Main { public static void main(String[] args) { Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class); map.put(DayOfWeek.MONDAY, "星期一"); map.put(DayOfWeek.TUESDAY, "星期二"); map.put(DayOfWeek.WEDNESDAY, "星期三"); map.put(DayOfWeek.THURSDAY, "星期四"); map.put(DayOfWeek.FRIDAY, "星期五"); map.put(DayOfWeek.SATURDAY, "星期六"); map.put(DayOfWeek.SUNDAY, "星期日"); System.out.println(map); System.out.println(map.get(DayOfWeek.MONDAY)); } }
TreeMap
HashMap 是无序的,sortedMap 会对key 进行排序, 而sortedMap 是接口。 TreeMap 是实现类:
使用TreeMap, 自定义的key必须实现Comparable 接口。 String, Integer 已实现了Comparable 接口。 如果没有实现Comparable 接口,则需要传入比较器: ```java public class Main { public static void main(String[] args) {
} }Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() { public int compare(Person p1, Person p2) { return p1.name.compareTo(p2.name); } }); map.put(new Person("Tom"), 1); map.put(new Person("Bob"), 2); map.put(new Person("Lily"), 3); for (Person key : map.keySet()) { System.out.println(key); } // {Person: Bob}, {Person: Lily}, {Person: Tom} System.out.println(map.get(new Person("Bob"))); // 2
// 注意, 如果比较元素相等的时候,应当返回0 public int compare(Student p1, Student p2) { if (p1.score == p2.score) { return 0; } return p1.score > p2.score ? -1 : 1; }
<a name="wCueq"></a>
## Set
Set 和 Map 一样,在放入自定义对象的时候,都需要实现equals 和 hashCode. HashSet 是无序的,TreeSet 实现了SortedSet, 是有序的。TreeSet 的使用要求和TreeMap 一致。
<a name="DtC8t"></a>
## Queue

<a name="yA8Y8"></a>
## PriorityQueue
优先队列,按照优先级FIFO<br />必须传入Comparator 用于比较优先级
<a name="y8vED"></a>
## Deque
<br />注意,Deque 是扩展了queue 的一个接口。 具体的实现类包括ArrayDeque 和 LinkedList。
<a name="rwanV"></a>
## Stack
先进后出的数据结构, 用deque 可以实现stack 的功能。 注意只调用push, pop, peek 方法。 避免使用deque 的其他方法。 deque 的实现类又包含ArrayDeque 和 LinkedList, 所以可以使用 LinkedList 来实现stack。
<a name="mJGtQ"></a>
## Iterator
注意, java 的集合类例如List, Set, Queue都可以使用for each 循环, Map 会迭代每个key。 编译器会把for each 循环编译为迭代器与for 循环:
```java
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
迭代器模式适用于在不知道集合内部结构的情况下遍历集合。 要使用for each 循环需要满足以下条件:
- 集合类实现iterable 接口,该接口要求返回一个iterator 对象
- 用iterator 迭代集合内部数据。适用于调用者对集合内部的情况一无所知。
Collections
Collections 是JDK 提供的工具类, 位于java.util 中,提供一系列静态方法。 注意Collections 是一个工具类,Collection 是 java 所有集合类的根接口:
创建空集合
List.of 创建多个元素的集合List<String> list1 = List.of(); List<String> list2 = Collections.emptyList();
Collections.sort(Collection) 可以对可变集合进行排序List<String> list1 = List.of(); // empty list List<String> list2 = List.of("apple"); // 1 element List<String> list3 = List.of("apple", "pear"); // 2 elements List<String> list4 = List.of("apple", "pear", "orange"); // 3 elements
Collections.shuffle(Collection) 洗牌可以随机打乱集合元素的顺序。RMI
远程方法调用指的是一个JVM中代码可以通过网络实现远程调用另一个JVM中的方法。 服务提供方为服务端,服务调用方为客户端。 实际RPC应用中。 客户端将参数序列化传递给服务端,服务端反序列化参数并传入到对应的方法,并将结果序列化返回给客户端。多线程
线程的创建
java 用Thread 对象表示一个线程。 通过调用start创建一个新线程。 线程的具体执行代码写在run 方法里。 Thread.sleep() 可以是当前线程睡眠一段时间。 创建线程的几种方法:
- 自定义Thread, 然后覆写run() 方法:
```java
public class Main {
public static void main(String[] args) {
} }Thread t = new MyThread(); t.start(); // 启动新线程
class MyThread extends Thread { @Override public void run() { System.out.println(“start new thread!”); } }
2. 创建Thread 实例,传入一个runnable 实例
```java
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
- 使用lambda 表达式进一步简写:
注意,要调用start 方法才能开启一个线程,start 调用JVM 底层的native 方法。 直接调用run 方法相当于调用普通的java 方法public class Main { public static void main(String[] args) { Thread t = new Thread(() -> { System.out.println("start new thread!"); }); t.start(); // 启动新线程 } }
线程状态
- New: 新创建的线程,尚未执行
- Runnable: 运行中的线程, 正在执行run 方法的java 代码
- Blocked: 在阻塞队列里的线程,此时不持有锁
- waiting: 在等待队列中的线程
- Timewaiting: 计时等待
- Terminated: 线程终止
注意: 通过调用另一个线程的joint() 方法可以等待其执行结束再执行。
中断线程
如果一个线程在执行长时任务,就需要能够中断一个线程。 要中断一个线程只需要在其他线程中对目标线程执行 interrupt() 方法,相当于向目标线程发送信号。 目标线程需要不停检查自身flag 是否是interrupted, 如果是就立刻结束运行状态。
- 如果线程处于等待状态,例如调用join。 此时调用Interrupt, join 方法会立即抛出InterruptedException。 同理, 只要捕获到了InterrupteExcpetion, 那么一定有其他线程对其调用了Interrupte 方法。
另一个常用的线程中断方法是设置标志位, 通过在外部线程中将running 标志位设置为false 来中断线程:
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
注意: running 是线程间共享变量,需要用volatile 来解决线程可见性问题。
volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。
注意:若线程在阻塞状态,调用了它的interrupt() 方法。 那么他的中断状态会被清除并受到一个InterruptedException.
守护线程(Daemon Thread)
如果某类线程是类似循环定时任务,一直不结束导致JVM也无法结束。 但是当其他线程结束时,JVM又必须结束,这时候就要用到守护线程。
守护线程是指为其他线程服务的线程,如果设置了守护线程。 当其他非守护线程结束后,JVM会自动退出。 如果没有设置,那么只要有一个线程不结束,JVM就不会结束。
线程同步方法
synchronized 关键字,锁共享对象或者锁方法。注意,放在方法上相当于锁住了当前对象。 对于static 方法是没有当前实例的,只能锁住当前的类。
死锁
一旦死锁形成就要强制结束进程。避免死锁的方法是多线程获取锁的顺序要一致。
Wait 和 Notify
用于线程间协同
- 在synchronized 内部调用被锁对象的wait() 方法使当前线程进入等待队列并释放锁。
- 注意:必须在已获得锁的对象上调用wait 方法。
- 在synchronized 内部可以调用notify 或 notifyall 方法来唤醒其他等待线程。
- 必须要在已获得锁的对象上调用notify 或者 notifyAll 方法。
- 已唤醒的线程要重新竞争锁。
ReentrantLock
可重入锁,即一个线程可以重复获取当前的锁
tryLock 可以尝试获取锁,避免空转造成死锁;
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
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();
}
}
}
Condition
配合ReetrantLock 实现notify 和 signal 功能。
- await 会释放当前锁,进入等待队列
- signal 唤醒某个等待线程
- signalAll 会唤醒所有等待线程, 重新竞争锁
Condition 可以指定等待时间,自己醒来,避免空转
if (condition.await(1, TimeUnit.SECOND)) { // 被其他线程唤醒 } else { // 指定时间内没有被其他线程唤醒 }
ReentrantLock 和 Condition 实现
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); condition.signalAll(); } finally { lock.unlock(); } } public String getTask() { lock.lock(); try { while (queue.isEmpty()) { condition.await(); } return queue.remove(); } finally { lock.unlock(); } } }
读写锁
上文的ReentrantLock 是悲观锁,有些情况下不需要悲观锁,比如读锁允许多个线程同时读,但不能写。从而提高并发性:
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
读写锁提高了读并发性,但是读的过程中不允许写。 StampedLock 读的时候允许其他线程写,引入了stamped 概念来判断前后读的时候stamped 是否发生变化来跟踪写更改,是乐观锁的一种。
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); } }
先创建一个乐观读锁返回版本号,再读区数据然后验证版本号。如果版本号发生变化则升级为悲观锁。
Concurrent 集合
用ReentrantLock 和 Condition 实现阻塞队列
public 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); condition.signalAll(); } finally { lock.unlock(); } } public String getTask() { lock.lock(); try { while (queue.isEmpty()) { condition.await(); } return queue.remove(); } finally { lock.unlock(); } } }
线程安全的集合类
线程池
创建一个线程需要线程资源,栈空间等。 频繁创建销毁线程消耗大量时间。 可以把一组小任务交给一组线程来执行,即线程池。 java 中ExecutorService 接口表示线程池:
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
常用的线程池:
- FixedThreadPool : 线程数固定
- CachedThreadPool: 线程数根据任务动态调整
- SingleThreadExecutor: 仅单线程执行的线程池
基本用法:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 创建一个固定大小的线程池:
ExecutorService es = Executors.newFixedThreadPool(4);
for (int i = 0; i < 6; i++) {
es.submit(new Task("" + i));
}
// 关闭线程池:
es.shutdown();
}
}
class Task implements Runnable {
private final String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("start task " + name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end task " + name);
}
}
注:
- shutdown 方法关闭线程会等任务执行完
- shutdownNow 会停止任务并立即关闭线程
- awaitTermination 会等待指定的时间让线程关闭,要和shutdown 配合使用
- CachedThreadPool 会自动调整线程数
scheduledThreadPool
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
执行一次性任务
// 1秒后执行一次性任务: ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
每三秒执行一次, 如果上一个任务没完成,会等待上个任务完成后立即执行下个任务。
// 2秒后开始执行定时任务,每3秒执行: ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
固定时间间隔
// 2秒后开始执行定时任务,以3秒为间隔执行: ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
Future
Runnable 接口没有返回值, 要处理有返回值的任务需要用callable 接口:
class Task implements Callable<String> { public String call() throws Exception { return longTimeCalculation(); } }
用future 来获取结果:
ExecutorService executor = Executors.newFixedThreadPool(4); // 定义任务: Callable<String> task = new Task(); // 提交任务并获得Future: Future<String> future = executor.submit(task); // 从Future获取异步执行返回的结果: String result = future.get(); // 可能阻塞当前线程
submit 会返回一个future 对象。 get 方法会阻塞当前线程直到任务完成
- get():获取结果(可能会等待)
- get(long timeout, TimeUnit unit):获取结果,但只等待指定的时间;
- cancel(boolean mayInterruptIfRunning):取消当前任务;
- isDone():判断任务是否已完成。
CompletableFuture
Future get 方法可能会阻塞线程。 CompletableFuture 可以异步执行任务,当任务完成后自动返回,不阻塞主线程。基本使用方法如下:
supplyAsync 需要一个supplier 对象, 可以用lambda 表达式或函数引用:public class Main { public static void main(String[] args) throws Exception { // 创建异步执行任务: CompletableFuture<Double> cf = CompletableFuture.supplyAsync(Main::fetchPrice); // 如果执行成功: cf.thenAccept((result) -> { System.out.println("price: " + result); }); // 如果执行异常: cf.exceptionally((e) -> { e.printStackTrace(); return null; }); // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭: Thread.sleep(200); }
thenAccept 函数接受一个consumer: ```java public interface Consumerpublic interface Supplier<T> { T get(); }
{ void accept(T t); }
异常时,会回调exceptionally 方法,传入一个 functional 对象:
```java
public interface Function<T, R> {
R apply(T t);
}
串行操作:
public class Main {
public static void main(String[] args) throws Exception {
// 第一个任务:
CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油");
});
// cfQuery成功后继续执行下一个任务:
CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
return fetchPrice(code);
});
// cfFetch成功后打印结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(2000);
}
并行操作:
public class Main {
public static void main(String[] args) throws Exception {
// 两个CompletableFuture执行异步查询:
CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://finance.sina.com.cn/code/");
});
CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://money.163.com/code/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);
// 两个CompletableFuture执行异步查询:
CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://finance.sina.com.cn/price/");
});
CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://money.163.com/price/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163);
// 最终结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(200);
}
anyof 可以实现任意一个CompletableFuture 只要一个成功, allof 表示所有的completableFuture 都必须成功。
方法命名规则:
- xxx: 表示方法将在已有的线程中执行
- xxxAsync: 表示将异步在线程中执行。
ForkJoin
利用forkJoinPool 和 RecursiveTask 实现分治算法:
https://www.liaoxuefeng.com/wiki/1252599548343744/1306581226487842ThreadLocal
每个线程都有一个ThreadLocalMap 属性,保存threadlocal 到局部变量的映射。 保证局部变量的线程隔离性。 基本用法:
注意,用完之后要及时删除threadlocal 里的内容, 因为线程用完后会被放入到线程池里,上一个线程的threadlocal 会影响到下次任务:void processUser(user) { try { threadLocalUser.set(user); step1(); step2(); } finally { threadLocalUser.remove(); } }
try { threadLocalUser.set(user); ... } finally { threadLocalUser.remove(); }
单元测试
使用fixture
@BeforeAll, @AfterAll, @BeforeEach, @AfterEach
- 对于实例变量,在beforeEach 中初始化,在afterEach 中清理。在各个test 中互不影响,因为是不同的实例。
- 对于静态变量,BeforeAll 中初始化, 在AfterAll 中销毁。 在各个test 方法中均是唯一实例, 会影响到各个test 方法。
注意:
最后,注意到每次运行一个@Test方法前,JUnit首先创建一个XxxTest实例,因此,每个@Test方法内部的成员变量都是独立的,不能也无法把成员变量的状态从一个@Test方法带到另一个@Test方法。
异常测试
用assertThrows 方法来测试是否正确抛出异常
@Test
void testNegative() {
assertThrows(IllegalArgumentException.class, new Executable() {
@Override
public void execute() throws Throwable {
Factorial.fact(-1);
}
});
}
executable 是一个函数式接口:
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface Executable {
void execute() throws Throwable;
}
可以用lambda 改写:
@Test
void testNegative() {
assertThrows(IllegalArgumentException.class, () -> {
Factorial.fact(-1);
});
}
条件测试
@Disabled 可以排出某些test 方法