2020-tplink提前批面经

2020 tplink提前批面经

  • 应聘岗位:IT软件工程师
  • 状态:一面过,二面没去

一面

  • 自我介绍
  • 介绍项目(Docker、DNS相关)
    • 项目简介
    • 介绍Docker
    • Docker和虚拟机的区别,相比虚拟机的优点
    • Docker和lxc的区别
    • DNS解析流程
  • Java
    • String和StringBuffer的区别,什么时候用哪个
    • Java如何避免内存溢出(没明白)-> 讲下gc

2020中兴提前批面经

2020中兴提前批面经

  • 应聘岗位:软件开发工程师
  • 状态:等待通知

专业面

  • 自我介绍
  • 介绍项目(Docker、DNS相关)
    • 项目简介(目的、实现技术)
    • 系统架构
    • Docker和虚拟机的区别
    • 如何监控Docker容器状态
    • Docker容器内进程和普通进程的区别
    • Docker的优缺点
    • DNS解析流程
  • Java和Go的区别
  • 数据库相关
    • 关系型和非关系型的区别
    • 解释事务
  • 解释递归(手撕递归的代码)
  • 没问Java,佛了
  • 关于Go(以后再也不说我会Go了,再也不!
    • Go的引用类型有哪些
    • Go的指针运算有哪些
    • 数组和切片的区别

HR面

  • 自我介绍
  • 应聘时选择企业考虑的因素
  • 希望在哪个城市工作,为什么
  • 讲一个在做项目时印象深刻的事
  • 自己的优缺点
  • 额外问题

部门leader(?)电话面

  • 介绍项目
  • 职业规划,三年后的目标
  • 期望薪资和原因
  • 期望工作城市
  • 是否接触过手机软件开发,介绍项目内容和项目去向
  • 假设你是项目负责人,如何着手完全没有接触过相关领域的项目
  • 接上,给定该项目的功能点,设想其服务段和客户端架构
  • 如果当前的项目毫无进展或者遇到重大问题,而第二天是项目的重要节点该怎么办
  • 空闲时间做什么

Java8实战笔记0x01

流简介

  • 流是从支持数据处理操作的源生成的元素序列。
  • 集合的核心是数据,而流的目的在于表达计算。
  • 和迭代器类似,流只能遍历一次。
  • 使用Collection接口需要用户做迭代,称为外部迭代。而使用Stream则代替用户做了迭代,称为内部迭代。

流操作

List<String> names = menu.stream()
                         // 中间操作
                         .filter(d -> d.getCalories())
                         .map(Dist::getName)
                         .limit(3)
                         // 终端操作
                         .collect(toList());
  • 诸如filtersorted等中间操作会返回另一个流,这让多个操作可以连接起来形成一个查询。除非流水线上触发一个终端操作,否则中间操作不会执行任何处理,因为中间操作一般都可以合并起来,在终端操作时一次性全部处理,这种技术称作循环合并。
  • 终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如ListInteger,甚至是void

使用流

筛选和切片

用谓词筛选

  • filter方法:接受一个谓词(返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。
List<Dish> vegetarianMenu = menu.stream()
                                .filter(Dish::isVegetarian)
                                .collect(toList());

提取不同元素

  • distinct方法:无参数,返回一个元素各异的流(根据流所生成元素的hashCode()equals()方法实现)。
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
       .filter(i -> i % 2 == 0)
       .distinct()
       .forEach(System.out::println);
/* Output:
2
4
*/

截短流

  • limit(n)方法:返回一个不超过给定长度n的流。如果流是有序的,则最多返回钱n个元素;如果流是无序的(例如Set),则返回结果不会以任何顺序排列。

跳过元素

  • skip(n)方法:返回一个去掉前n个元素的流。如果流中的元素不足n个,则返回一个空流。与limit(n)是互补操作。

映射

对流中的每个元素应用函数

  • map方法:接受一个函数作为参数,这个函数会被应用到每个元素上,并将其映射成一个新的元素(与转换的区别是,该步骤是创建一个新的版本而不是修改)。
List<String> dishName = menu.stream()
                            .map(Dish::getName)
                            .collect(toList());

流的扁平化

  • flatMap方法:传入的方法生成的流不会各自独立,而是会被合并,扁平化称为一个流。

查找和匹配

  • anyMatch检查谓词是否至少匹配一个元素;allMatch会检查谓词是否匹配所有元素,相对的noneMatch则会确保流中没有任何元素与给定的谓词匹配。这三种方法都用到了短路。
  • findAny可以返回流中的任意元素,而findFirst则返回有出现顺序的流中的第一个元素。使用时配合Optional<T>使用。此外findAny在使用并行流时限制较少,如果不关心返回哪个元素,那么应该使用findAny

归约

int sum = numbers.stream().reduce(0, (a, b) -> a + b);
  • 上面代码展示了用reduce求一组整数的和,reduce接受一个初始值和一个函数,它将使用函数反复结合每个元素,直到流被归约成一个值。
  • 使用reduce可以让内部实现得以选择并行执行操作。但是,传递给reduce的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。

数值流

  • IntStreamDoubleStreamLongStream分别将流中的元素特化为intdoublelong,从而避免的暗含的装箱成本。数值流包含进行常用归约的方法,如求和sum,找到最大元素max等。
  • 映射到数值流可以用mapToIntmapToDoublemapToLong,而使用boxed方法可以将数值流转回对象流(各个基本类型对应的包装类型)。
  • rangerangeClosed是可以用于IntStreamLongStream的静态方法,作用是生成范围内的数,区别是range不包含结束值而rangeClosed包含。

构建流

由值创建流

  • 使用Stream.of可以通过显式值创建一个流,可以接受任意数量的参数。而Stream.empty可以得到一个空流。
Stream<String> stream = Stream.of("Java 8", "Lambdas", "In", "Action");
Stream<String> emptyStream = Stream.empty();

由数组创建流

  • 静态方法Arrays.stream可以从数组创建一个流,接受一个数组作为参数。

由文件生成流

  • java.nio.file.Files中的很多静态方法都会返回一个流,例如Files.lines,它会返回一个由指定文件中的各行构成的字符串流。

由函数生成流——创建无限流

迭代

  • iterate方法接受一个初始值,以及一个一次应用在每个产生的新值上的Lambda。iterate操作基本上是顺序的,因为结果取决于前一次应用。该操作将生成一个无限流,没有结尾,因为值是按需计算的,可以永远计算下去。因此流和集合的一个关键的区别在于流是无界的。

生成

  • generate方法也可以生成无限流,它接受一个Supplier<T>类型的Lambda提供新的值。

Java8实战笔记0x00

通过行为参数化传递代码

行为参数化

  • 谓词:根据对象的某些属性来返回boolean值的函数。

代码进化节选

使用接口实现策略设计模式

// 顶层接口
public interface ApplePredicate {
    boolean test(Apple apple);
}
// 各种具体实现,可以不断增加
public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}
public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}
// 使用部分,无需变动
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple: inventory) {
        if (p.test(apple)) result.add(apple);
    }
    return result;
}

使用匿名内部类

  • 在上面代码中,所定义的类可能仅实例化一次, 而使用匿名内部类可以弥补这点。
// 仍然需要上面代码的顶层接口,但无需实际定义类
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
    public boolean test(Apple a) {
        return "red".equals(a.getColor());
    }
});

使用Lambda表达式

List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

使用泛型

public interface Predicate<T> {
    boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for (T e: list) {
        if (p.test(e)) result.add(e);
    }
    return result;
}
//...
List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);

Lambda表达式

使用Lambda表达式

  • 函数式接口:是指只定义一个抽象方法的接口。例如Java API中的ComparetorRunnableCallable等。
  • @FunctionalInterface:用于标注接口为函数式接口,如果该接口没有定义抽象方法或者包含多个抽象方法,编译时会出现错误。其作用类似于@Override

使用函数式接口

  • java.util.function.Predicate<T>提供一个test()方法,接受一个泛型T对象,返回一个boolean
  • java.util.function.Consumer<T>提供一个accept()方法,接受一个泛型T对象,返回为void
  • java.util.function.Function<T, R>提供一个apply()方法,接受一个泛型T对象,返回一个泛型R对象。

原始类型特化

  • 虽然Java可以进行自动装箱和拆箱,但是这一操作还是有性能代价的。为避免自动装箱和拆箱,Java为其提供的函数式接口提供了原始类型特有版本,例如功能类似Predicate<T>IntPredicate,其参数为int而不是Integer可以避免装箱。

异常处理

  • 任何函数式接口都不允许抛出受检异常。如果希望Lambda表达式可以抛出异常,可以定义一个自己的函数式接口,并声明受检异常,或者把Lambda主体用try/catch块包裹。下面代码为示例。
@FunctionalInterface
public interface BufferredReaderProcessor {
    String process(BufferedReader b) throws IOException;
}
//...
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
Function<BufferedReader, String> f = (BufferedReader b) -> {
    try {
        return b.readLine();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
};

类型推断

  • 编译器会自动推断Lambda表达式的参数类型,而无需显式声明。
// 没有类型推断
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 有类型推断
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

使用局部变量

  • Lambda表达式允许使用自由变量(在外层作用域中定义的变量),类似于匿名类。
  • Lambda可以没有限制的捕获实例变量和静态变量,但是局部变量必须声明为final或者事实上是final。关于事实上是final的变量,见下面代码。不符合事实上是final的变量出现时,编译器将会发出错误信息。
int n = 1337;
Runnable r = () -> System.out.println(n); 
// Error: java: 从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量
n = 31337; 

方法引用

  • 方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。如果一个Lambda代表的只是直接调用这个方法,那最好还是用名称调用而不是尝试去描述过程。事实上,方法引用就是根据已有的方法实现来创建Lambda表达式。
(Apple a) -> a.getWeight(); 
// 等价于
Apple::getWeight

分类

  • 方法引用主要有三类:

    1. 指向静态方法的方法引用(例如IntegerparseInt(),写作Integer::parseInt
    2. 指向任意类型实例方法的方法引用(例如String对象的length(),写作String::length
    3. 指向现有对象的实例方法的方法引用(例如局部变量x,其拥有实例方法getValue(),则可写x::getValue

    注意,第二种实例是Lambda本身的参数,而第三种实例来自外部。

构造函数引用

  • 可以使用ClassName::new来对构造函数进行引用。注意选择正确的接口接收构造函数引用,见下面代码示例。
class Apple {
    private int w;

    public Apple() {
        w = 10;
    }

    public Apple(int w) {
        this.w = w;
    }
    public static void main(String[] args) throws Exception {
        Supplier<Apple> c = Apple::new;
        Apple a1 = c.get();
        Function<Integer, Apple> c2 = Apple::new;
        Apple a2 = c2.get(20);
    }
}

ClassName::new会选择匹配函数式接口对应方法参数列表的构造函数,而无需指定参数列表。

复合Lambda表达式

  • Java 8的一些函数式借口都有为方便而设计的方法,可以把多个简单的Lambda复合成复杂的表达式(例如使用or连接两个Lambda)。这些方法的实现使用了接口的默认方法。

比较器复合

以一个Comparator为例:

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

逆序

  • 如果希望将结果逆序输出,可以使用默认方法reversed()将比较器结果逆序:
inventory.sort(comparing(Apple::getWeight).reversed());

比较器链

  • 如果使用比较器得到两个应该处于相同位置的对象时,可以使用thenComparing()来进一步比较:
inventory.sort(comparing(Apple::getWeight))
         .reversed()
         .thenComparing(Apple::getCountry));

谓词复合

  • 谓词借口包括三个方法:negateandor,其优先级是从左往右。
// 从现有的Predicate创建结果的非
Predicate<Apple> notRedApple = redApple.negate();
// 使用and和or创建复杂的Predicate对象
Predicate<Apple> p = redApple.and(a -> a.getWeight() > 150)
                             .or(a -> "green".equals(a.getColor()));

函数复合

  • 可以使用Function接口将Lambda表达式复合起来。其中提供andThen()compose()两个方法,它们都会返回一个Function实例。andThen()会先对输入应用调用该方法的Function,后对输入应用该方法的Function参数;compose()则是先对输入应用该方法的Function参数,后对输入应用调用该方法的Function

Java编程思想笔记0x15

并发(二)

共享受限资源

不正确地访问资源

class Even {
    private int i = 0;

    public int next() {
        ++i; // [1]
        ++i;
        return i;
    }
}
  • 上面代码中,如果正确执行,那么next()方法返回的一定是偶数。但是在并发的条件下,[1]处如果线程被挂起,被另外一个线程调用该对象的next()方法时则会返回奇数。

使用synchronize进行同步控制

class Even {
    private int i = 0;

    public synchronized int next() {
        ++i; // [1]
        ++i;
        return i;
    }
}
  • 使用synchronized关键字进行同步控制,应将涉及竞争的数据成员都声明为private,仅允许通过方法来访问这些成员。然后将相关的方法声明为synchronized
  • 所有对象都自动含有单一的锁(也成为监视器)。当在对象上调用其任意的synchronized方法的时候,此对象都被加锁,此时该对象上的其他synchronized方法只有等前一个方法调用完毕并释放了锁之后才能被调用。
  • 一个任务可以多次获得对象的锁。JVM会跟踪加锁的次数,每当这个相同的任务在这个对象上获得锁时,相应的计数器会递增;每当任务离开一个synchronized方法,计数会递减;当计数为0时,锁被完全释放,此时其他任务可以使用此资源。
  • 针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static方法可以在类的范围内防止对static数据的并发访问。

使用显式的Lock对象

  • Lock对象必须被显式地创建、锁定和释放。相比起synchronized,代码缺乏优雅性,但是更加灵活;并且,如果使用synchronized时某些事物失败的将会抛出异常,没有机会进行清理工作,而使用Lock对象可以在finally子句中进行清理工作。
class Even {
    private int i = 0;
    private Lock lock = new ReentrantLock();

    public int next() {
        lock.lock();
        try {
            ++i;
            ++i;
            return i;
        } finally {
            lock.unlock();
        }
    }
}

原子性和可视性

  • 原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定在可能发生的上下文切换之前执行完毕。

当变量声明为volatile时,变量将具备以下两个特性:

  1. 保证此变量对所有线程的可见性,即一个线程修改了volatile变量后,其余线程可以立即获得修改后的值。

  2. 禁止指令重排序优化,即设置内存屏障,保证volatile变量修改更新到所有CPU上。

  • 在非volatile上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到这个新值。
  • 如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的。
  • 当一个域的值依赖于它之前的值,或者受到其他域的值的限制时,volatile将无法工作。

原子类

  • Java SE5引入了诸如AtomicIntegerAtomicLongAtomicReference等特殊的原子性变量类,提供原子性条件更新操作compareAndSet(expectedValue, updateValue),这是由硬件(CPU指令)支持的。

临界区

  • 如果只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个代码,可以使用synchronized指定某个对象,此对象的锁将用于对指定代码段进行同步控制。这段代码被称为临界区,或者同步控制块。
  • 一般情况下,使用synchronized同步临界区时指定的同步对象为当前对象,即synchronized (this)。当然也可以指定其它对象,但注意确保所有任务都是在同一个对象上同步的。

终结任务

线程状态

  • 一个线程可以处于一下四种状态之一:
    1. 新建:当线程被创建时,它只会短暂地处于该状态。此时它已经得到了必需的系统资源,并进行了初始化,可以获得CPU时间了。之后调度器将把这个线程转变为可运行状态或者阻塞状态。
    2. 就绪:在这种状态下,只要调度器把时间片分配给线程,线程就可以运行,也就是说,线程可以运行也可以不运行。
    3. 阻塞:线程能够运行,但有某个条件阻止它运行。当线程处于阻塞状态时,调度器将会忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,它才有可能执行操作。
    4. 死亡:处于死亡或终止状态的线程不再是可调度的,并且再也不会得到CPU时间,它的任务已经结束,或者不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。

进入阻塞状态

  • 一个任务进入阻塞状态,可能有如下原因:
    1. 通过调用sleep()使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。
    2. 调用wait()使线程挂起,直到线程得到了notify()或者notifyAll()消息(或者在Java中等价的signal()signalAll()消息),线程才会进入就绪状态。
    3. 任务在等待某个输入/输出完成。
    4. 任务试图在某个对象上调用其同步控制方法,但对象锁不可用,因为另一个任务已经获取了这个锁。

中断

  • Thread类包含interrupt()方法,可以终止被阻塞任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将跑出InterruptedException。当抛出该异常或者该任务调用Thread#interrupted()时,中断状态将被复位。
  • 如果在Executor上调用shutdownNow(),那么它将发送一个interrrupt()调用给它启动的所有线程。如果希望只中断某个任务,那么需要通过调用submit()而不是executor()启动任务,这样可以持有线程的上下文。submit()将放回一个泛型Future<?>,可以在其上调用cancel(),由此中断某个任务。如果将true传递给cancel(),那么它就会有在该线程上调用interrupt()以停止这个线程的权限。
  • interrupt()不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程,因为操作系统并未提供该功能
  • 可以通过调用Thread.interrupted()来检查中断状态,不仅仅可以得知interrupt()是否被调用过,还可以清除中断状态。

Java编程思想笔记0x14

并发(一)

基本的线程机制

定义任务

class LiftOff implements Runnable {
    protected int countDown = 10;
    private static int taskCount = 0;
    private final int id = taskCount++;
    public LiftOff() {}
    public LiftOff(int countDown) {
        this.countDown = countDown;
    }
    public String status() {
        return String.format("#%d(%s)", id, countDown > 0? countDown:"LiftOff");
    }
    @Override
    public void run() {
        while (countDown-- > 0) {
            System.out.println(status());
            Thread.yield();
        }
    }
}
  • 实现Runnable接口并实现run()方法,即可执行自定义任务。
  • 静态方法Thread.yield()是对线程调度器的一种建议,告知其可以让出CPU转移给其它线程。

Thread类

public class Main {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 5; i++) {
            new Thread(new LiftOff()).start();
        }
    }
}
  • 可以直接继承Thread类,或者向Thread构造器中传入Runnable对象,来构建由不同线程执行任务的对象。
  • 上面代码一次执行的结果可能和另外一次不同,因为线程调度机制是非确定的。

Executor

  • Executor能够管理异步任务的执行,无须显式地管理线程的生命周期。
public class Main {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            exec.execute(new LiftOff());
        }
        exec.shutdown();
    }
}
  • 在上面代码中,ExecutorService对象使用静态方法Executors.newCachedThreadPool()创建,ExecutorService#execute()为每一个任务都创建了一个线程。
  • ExecutorService#shutdown()方法可以防止新任务提交给该Executor,而当前线程将继续运行在shutdown()被调用之前提交的所有任务,待所有任务均执行完毕后,当前线程将会尽快退出。
  • ChchedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程。
public class Main {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            exec.execute(new LiftOff());
        }
        exec.shutdown();
    }
}
  • FixedThreadPool可以一次性预先执行代价高昂的线程分配,因而也可以限制线程的数量。由于不需要为每个任务都固定付出创建线程的开销,因此可以节省时间。在事件驱动的系统中,需要线程的事件处理器,通过直接从池中获取线程以尽快得到服务。在这个过程中,由于线程总数是有限的,因此不会出现资源滥用。
  • 在任何线程池中,现有线程在可能的情况下都会被自动复用。
  • SingleThreadExecutor类似于线程数量为1的FixedThreadPool,可以用于在另一个线程中连续运行任何事物(长期服务),例如监听进入的套接字连接的任务。如果向SingleThreadExecutor提交了多个任务,那么这些任务将会排队执行,所有的任务将使用相同的线程。这能够保证在任意时刻在任意线程中都只有唯一的任务在运行,不需要在共享资源上处理同步并有限制的使用这些资源。

从任务中产生返回值

  • 如果需要任务在完成时能够返回一个值,可以实现Callable接口。Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()中返回值的类型。向Executor提交任务时使用ExecutorService#submit()
class Test implements Callable<String> {
    private int id;
    public Test(int id) {
        this.id = id;
    }

    @Override
    public String call() throws Exception {
        return "result: " + id;
    }
}
public class Main {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(5);
        List<Future<String>> result = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            result.add(exec.submit(new Test(i)));
        }
        for (Future<String> r: result) {
            try {
                System.out.println(r.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } finally {
                exec.shutdown();
            }
        }
        exec.shutdown();
    }
}
  • ExecutorService#submit()方法会产生Future对象,它用Callable返回的结果的特定类型进行参数化。可以调用Future#isDone()来查询Future是否已经完成,如果完成可以调用Future#get()获取对应结果。也可以直接调用Future#get(),此时会发生阻塞,直到结果准备就绪。

休眠

  • 可以调用TimeUnit.MILLSECONDS.sleep()来中止任务执行给定的时间,同时会抛出InterruptedException异常。由于异常不能跨线程传播回主线程,因此必须在本地处理所有在任务内部产生的异常。

优先级

  • 线程的优先级将该线程的重要性传递给了调度器,调度器将倾向度让优先级最高的线程先执行。然而,这并不意味着优先级较低的线程得不到执行,仅仅是执行的频率较低。
class Test implements Runnable {
    private int countDown = 5;
    private volatile double d;
    private int priority;
    public Test(int priority) {
        this.priority = priority;
    }
    public String toString() {
        return Thread.currentThread() + ": " + countDown;
    }

    @Override
    public void run() {
        Thread.currentThread().setPriority(priority);
        while(true) {
            for (int i = 1; i < 100000; i++) {
                d += (Math.PI + Math.E) / (double)i;
                if (i % 1000 == 0) Thread.yield();
            }
            System.out.println(this);
            if (--countDown == 0) return;
        }
    }
}
public class Main {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            exec.execute(new Test(Thread.MAX_PRIORITY));
            exec.execute(new Test(Thread.MIN_PRIORITY));
        }
    }
}
  • 可以使用getPriority()来读取现有线程的优先级,并且可以在任何时候通过调用setPriority()来修改优先级。
  • 尽管JDK有10个优先级,但是和多数的操作系统都不能映射得很好。唯一可移植的方法是只使用MAX_PRIORITYNORM_PRIORITYMIN_PRIORITY

后台线程

  • 后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中所有的后台线程。
  • 可以使用Thread#setDaemon()来设置线程为后台线程,但必须在线程启动之前设置。
class DaemonThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
    }
}
class Daemon implements Runnable {
    @Override
    public void run() {
        try {
            while (true) {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() + " " + this);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory());
        for (int i = 0; i < 10; i++) {
            exec.execute(new Daemon());
        }
        System.out.println("all start.");
        TimeUnit.MILLISECONDS.sleep(500);
    }
}
  • 每个静态的ExecutorService创建方法可以被重载为接收一个ThreadFactory对象,而这个工厂对象将用于创建新的线程。在上面代码中,自定义的工厂将新的线程均设置成为后台线程。
  • 可以通过调用Thread#isDaemon()方法来确定线程是否为一个后台线程。后台线程创建的线程都将自动设置成为后台线程。
class Daemon implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("starting daemon");
            TimeUnit.SECONDS.sleep(1); // [1]
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("run?");
        }
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Daemon());
        t.setDaemon(true);
        t.start();
    }
}
  • 当最后一个非后台线程终止时,JVM就会立即关闭所有后台进程(例如上面代码,尽管执行到[1]处会被强制停止,但不会执行finally子句)。这不是关闭后台进程优雅的方式,相应的,非后台的Executor控制的所有任务可以同时被关闭,这是一种更好的解决方式。

加入一个线程

  • 一个线程可以在其他线程之上调用join()方法,此时调用join()的线程将被挂起,直到目标线程结束才回复(即t.isAlive为假)。也可以在调用join()时带上一个超时参数,如果发生超时目标线程还没有结束,join()方法也会返回。

捕获线程异常

class ThrowException implements Runnable {
    @Override
    public void run() {
        throw new RuntimeException();
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        try {
            exec.execute(new ThrowException());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 上面代码中的try语句无法捕获线程中的异常。
class ThrowException implements Runnable {
    @Override
    public void run() {
        throw new RuntimeException();
    }
}

class CatchException implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught: " + e);
    }
}

class ExceptionThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setUncaughtExceptionHandler(new CatchException());
        return t;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool(new ExceptionThreadFactory());
        exec.execute(new ThrowException());
    }
}
/* Output:
caught: java.lang.RuntimeException
*/
  • Thread.UncaughtException接口可以为每个Thread对象都附着一个异常处理器。其中uncaughtException()方法会在线程因未捕获异常而退出前被调用。上面代码中创建了一个ThreadFactory子类来为线程添加异常处理器。

Java编程思想笔记0x13

注解

  • 注解也被称为元数据,为在代码中添加信息提供了一种形式化的方法,以便在后面使用这些数据。
  • 有三种内置的注解,分别是:
    1. @Override,表示当前的方法定义将覆盖超类中的方法。如果注解的方法没有对应到超类中的方法,编译器会发出错误提示。
    2. @Deprecated,如果在其它代码中使用了注解为@Deprecated的元素,那么编译器会发出警告信息。
    3. @SuppressWarnings,关闭不当的编译器警告信息。

基本语法

定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Test {}
  • 除了@以外,@Test的定义很像一个空的接口。
  • 没有元素的注解称为标记注解。

元注解

元注解 说明
@Target 表示该注解可以用于什么地方,可能的参数包括:CONSTRUCTOR(构造器声明),FIELD(域声明),LOCAL_VARIABLE(局部变量声明),METHOD(方法声明),PACKAGE(包声明),PARAMETER(参数声明),TYPE(类、接口、注解、枚举)
@Retention 表示需要再什么级别保存该注解信息。可选的参数包括:SOURCE(注解将被编译器丢弃),CLASS(注解在class文件中可用,但会被VM丢弃),RUNTIME(VM在运行期也会保留注解,可以通过反射读取注解信息)
@Documented 将此注解包含在Javadoc中
@Inherited 允许子类继承父类中的注解

编写注解处理器

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface UseCase {
    public int id();

    public String desc() default "No description";
}

class PasswordUtils {
    @UseCase(id = 47, desc = "Need one or more numeric")
    public boolean validatePassword(String password) {
        return (password.matches("\\w*\\d\\w*"));
    }
    @UseCase(id = 48)
    public String encryptPassword(String password) {
        return new StringBuilder(password).reverse().toString();
    }
    @UseCase(id = 49, desc = "Same with the previous")
    public boolean chekcForNewPassword(List<String> prevPasswords, String password) {
        return !prevPasswords.contains(password);
    }
}

public class Main {
    public static void trackUseCases(List<Integer> useCases, Class<?> cl) {
        for (Method m: cl.getDeclaredMethods()) {
            UseCase uc = m.getAnnotation(UseCase.class);
            if (uc != null) {
                System.out.println(uc.id() + " " + uc.desc());
                useCases.remove((Integer)uc.id());
            }
        }
        for (int i: useCases) {
            System.out.println(i + "Not found");
        }
    }
    public static void main(String[] args) throws Exception {
        List<Integer> useCases = new ArrayList<>();
        Collections.addAll(useCases, 47, 48, 49, 50);
        trackUseCases(useCases, PasswordUtils.class);
    }
}
  • 上面代码使用了反射来取得注解信息。其中,getAnnoation()方法返回指定类型的注解对象。如果被注解方法上没有该类型的注解,则返回null值。如果注解中没有指定注解字段值,则返回定义注解字段时的默认值。

注解元素

  • 注解元素可用的类型:

    • 所有基本类型
    • Class
    • enum
    • String
    • Annotation

    如果使用了其它类型则会报错。另外,注解可以作为元素的类型,这意味着注解可以嵌套。

默认值限制

  • 元素必须有默认值,或者在使用注解时提供元素的值。
  • 对于非基本类型的元素,无论是在源代码中,还是在注解接口中定义默认值,都不能以null作为其值。

全排列

全排列

递归方法

public class Main {
    static void swap(int[] arr, int i, int j) {
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }
    static void full(int[] arr, int start) {
        if (start == arr.length - 1) System.out.println(Arrays.toString(arr));
        else {
            // 将当前最左边和后面的值依次交换
            for (int i = start; i < arr.length; i++) {
                swap(arr, start, i);
                // 递归解决子数组全排列
                full(arr, start + 1);
                // 复原数组
                swap(arr, start, i);
            }
        }
    }
    public static void main(String[] args) throws Exception {
        full(new int[]{1,2,3,4}, 0);
    }
}

字典排序

public class Main {
    static void dict(int[] arr) {
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
        while (true) {
            // 从最右起,找出为升序的相邻两个数,记录小的位置i
            int i;
            for (i = arr.length - 2; i >= 0; i--) {
                if (arr[i] < arr[i + 1]) break;
            }
            // 如果没有则表示完成字典排序
            if (i == -1) break;
            // 从最右起,找出比arr[i]大的第一个数,记录位置j
            int j;
            for (j = arr.length - 1; j > 0; j--) {
                if (arr[j] > arr[i]) break;
            }
            // 交换i和j
            int t = arr[i];
            arr[i] = arr[j];
            arr[j] = t;
            // 对i + 1到最后进行排序
            Arrays.sort(arr, i + 1, arr.length);
            System.out.println(Arrays.toString(arr));
        }
    }
    public static void main(String[] args) throws Exception {
        dict(new int[]{1, 1, 2, 4});
    }
}

Java编程思想笔记0x12

枚举类型(二)

使用EnumSet替代标志

  • 是一种面向enum的标志,可以用来表示某种“开/关”信息。其内部实现是将一个long值作为位向量,因此EnumSet非常高效。
enum AlarmPoints {
    STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3,
    OFFICE4, BATHROOM, UTILITY, KITCHEN
}

public class Test {
    public static void main(String[] args) throws Exception {
        EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class);
        points.add(AlarmPoints.BATHROOM);
        System.out.println(points);
        points.addAll(EnumSet.of(AlarmPoints.STAIR1, AlarmPoints.STAIR2, AlarmPoints.KITCHEN));
        System.out.println(points);
        points = EnumSet.allOf(AlarmPoints.class);
        points.removeAll(EnumSet.of(AlarmPoints.STAIR1, AlarmPoints.STAIR2, AlarmPoints.KITCHEN));
        System.out.println(points);
        points.removeAll(EnumSet.range(AlarmPoints.OFFICE1, AlarmPoints.OFFICE4));
        System.out.println(points);
        points = EnumSet.complementOf(points);
        System.out.println(points);
    }
}
  • EnumSet.of()方法被重载了很多次,不仅为可变数量参数进行类重载,还为接收1到5个显式的参数的情况进行类重载。即当使用2到5个参数时,会调用对应的重载方法,而使用1个或者多于5个参数时,会调用可变参数的of()方法。注意,1个参数不会导致编译器构造可变参数数组。
  • 在JDK 11中,EnumSet.of()不同的重载方法有:1至5个显式声明参数的方法,和一个参数列表为(E first, E... rest)的方法。

  • 在创建EnumSet对象时,如果枚举实例小于等于64个,则使用一个long值,否则使用一个long数组。

使用EnumMap

  • EnumMap要求其中的键必须来自一个enum。由于枚举实例总数量是确定的,EnumMap内部使用了数组实现。调用put()方法时键只能是枚举实例,其它操作和普通的Map基本一致。
enum AlarmPoints {
    STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3,
    OFFICE4, BATHROOM, UTILITY, KITCHEN
}
interface Command {
    void action();
}

public class Test {
    public static void main(String[] args) throws Exception {
        EnumMap<AlarmPoints, Command> em = new EnumMap<>(AlarmPoints.class);
        em.put(AlarmPoints.KITCHEN, new Command() {
            public void action() {
                System.out.println("Kichen fire!");
            }
        });
        em.put(AlarmPoints.BATHROOM, new Command() {
            public void action() {
                System.out.println("Bathroom alart!");
            }
        });
        for(Map.Entry<AlarmPoints, Command> e: em.entrySet()) {
            System.out.print(e.getKey() + ": ");
            e.getValue().action();
        }
        try {
            em.get(AlarmPoints.UTILITY).action();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}
/* Output:
BATHROOM: Bathroom alart!
KITCHEN: Kichen fire!
java.lang.NullPointerException
*/
  • 上面代码是一种命令模式的用法,即首先需要一个只有单一方法的接口,然后从该接口实现具有各自不同的行为的多个子类。之后可以根据需要构造命令对象并使用。
  • 如果EnumMap没有为某个键调用put()方法来存入相应的值的话,那么其对应的值就为null
  • 注意foreach中元素的泛型声明不可省略

常量相关方法

  • 可以为enum实例编写方法,从而为每个枚举实例赋予各自不同的行为。首先需要为enum定义一个或多个方法(或者抽象方法),然后为选择枚举实例进行覆盖方法(如果是抽象方法则每个枚举实例都要实现该方法)。
public enum Test {
    DATE_TIME {
        String getInfo() {
            return DateFormat.getDateInstance().format(new Date());
        }
    }, 
    CLASSPATH {
        String getInfo() {
            return System.getenv("CLASSPATH");
        }
    }, 
    VERSION {
        String getInfo() {
            return System.getProperty("java.version");
        }
    },
    TEST {
        String getInfo() {
            return "test info";
        }
        int getNumber() {
            return 222222;
        }
    };
    abstract String getInfo();
    int getNumber() {
        return 111111;
    }
    public static void main(String[] args) throws Exception {
        for (Test t: values()) {
            System.out.println(t.getInfo() + " " + t.getNumber());
        }
    }
}
/* Output:
2019年7月21日 111111
.;C:\Program Files\Java\jdk-11.0.3\lib\dt.jar;C:\Program Files\Java\jdk-11.0.3\lib\tools.jar; 111111
11.0.3 111111
test info 222222
*/
  • 通过相应的枚举实例,可以调用其上的方法,这通常也称为表驱动的代码(tabledriven code)。在上面代码中,枚举实例似乎被当做其“超类”来使用,在调用覆盖(实现)的方法(抽象方法)时体现多态的行为。

使用enum职责链

  • 在职责链设计模式中,可以用多种不同的方法来解决一个问题,然后将它们链接在一起。当一个请求到来时,遍历这个链,直到链中的某个解决方案能够处理该请求。
  • 通过常量相关的方法,可以很容易地实现一个简单的职责链。其中每一次尝试可以看做一个策略(设计模式),完整的处理方式列表就是一个职责链。
enum Handle {
    PLUS {
        boolean modify(int src, int target) {
            switch(target - src) {
                case 1: 
                    System.out.println("plus");
                    return true;
                default: return false;
            }
        }
    },
    MINUS {
        boolean modify(int src, int target) {
            switch(target - src) {
                case -1: 
                    System.out.println("minus");
                    return true;
                default: return false;
            }
        }
    },
    ABS {
        boolean modify(int src, int target) {
            switch(target + src) {
                case 0: 
                    System.out.println("abs");
                    return true;
                default: return false;
            }
        }
    };
    abstract boolean modify(int src, int target);
} 
public class Test {
    static void modify(int src, int target) {
        System.out.print(src + " ");
        for(Handle h: Handle.values()) {
            if(h.modify(src, target)) return;
        }
        System.out.println("can't modify");
    }
    public static void main(String[] args) throws Exception {
        Random r = new Random(47);
        int target = r.nextInt(10) - 5;
        System.out.println(target);
        for (int i = 0; i < 10; i++) {
            int src = r.nextInt(10) - 5;
            modify(src, target);
        }
    }
}
/* Output:
3
0 can't modify
-2 can't modify
-4 can't modify
-4 can't modify
4 minus
3 can't modify
-5 can't modify
-3 abs
2 plus
3 can't modify
*/

使用enum的状态机

  • 枚举类型非常适合用来创建状态机。一个状态机有有限的特定状态,它通常根据输入,从一个状态转移到下一个状态。每个状态都具有某些可接受的输入,不同的输入回事状态机从当前状态转移到不同的新状态。由于enum对其实例有严格的限制,非常适合用来表现不同的状态和输入。
enum Check {
    EVEN {
        void next(char n) {
            switch(n) {
                case '0':
                    c = ODD;
                    return;
                case '1':
                    return;
                default:
                    c = ERROR;
            }
        }
    },
    ODD {
        void next(char n) {
            switch(n) {
                case '0':
                    c = EVEN;
                    return;
                case '1':
                    return;
                default:
                    c = ERROR;
            }
        }
    },
    ERROR;
    private static Check c;
    void next(char n) { throw new RuntimeException("no such method."); };
    public static void check(String num) {
        c = EVEN;
        char[] ns = num.toCharArray();
        for (char n: ns) {
            c.next(n);
            if(c == ERROR) break;
        }
        switch(c) {
            case ODD:
                System.out.println("odd 0s");
                return;
            case EVEN:
                System.out.println("even 0s");
                return;
            case ERROR:
                System.out.println("incorrect input");
        }
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        Check.check("01001100001");
        Check.check("000011110000");
        Check.check("00110020");
    }
}
/* Output:
odd 0s
even 0s
incorrect input
*/
  • 上面代码中实现了判断一串字符串的二进制数中有奇数还是偶数个0。有三种状态,EVENODDERROR,前两种状态根据输入的不同决定将要转移的下一个状态,而ERROR则表示输入有误,仅作为最终状态。

多路分发

  • 如果存在一种多元运算,其变量类型范围确定但是不能知道某个变量的具体类型,并且该运算结果与变量类型相关,即该运算的实现需要判断各个参数的类型,那么此时可以使用多路分发。
enum Outcome { WIN, DRAW, LOSE}
interface Item {
    Outcome compete(Item it);
    Outcome eval(Paper p);
    Outcome eval(Scissors s);
    Outcome eval(Rock r);
}
class Paper implements Item {
    public Outcome compete(Item it) { return it.eval(this); }
    public Outcome eval(Paper p) { return Outcome.DRAW; }
    public Outcome eval(Scissors s) { return Outcome.WIN; }
    public Outcome eval(Rock r) { return Outcome.LOSE; }
    public String toString() { return "Paper"; }
}
class Scissors implements Item {
    public Outcome compete(Item it) { return it.eval(this); }
    public Outcome eval(Paper p) { return Outcome.LOSE; }
    public Outcome eval(Scissors s) { return Outcome.DRAW; }
    public Outcome eval(Rock r) { return Outcome.WIN; }
    public String toString() { return "Scissors"; }
}
class Rock implements Item {
    public Outcome compete(Item it) { return it.eval(this); }
    public Outcome eval(Paper p) { return Outcome.WIN; }
    public Outcome eval(Scissors s) { return Outcome.LOSE; }
    public Outcome eval(Rock r) { return Outcome.DRAW; }
    public String toString() { return "Rock"; }
}
public class Test {
    static final int SIZE = 10;
    private static Random r = new Random(47);
    public static Item newItem() {
        switch(r.nextInt(3)) {
            default:
            case 0: return new Scissors();
            case 1: return new Rock();
            case 2: return new Paper();
        }
    }
    public static void match(Item a, Item b) {
        System.out.println(a + " vs. " + b + ": " + a.compete(b));
    }
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < SIZE; i++) {
            match(newItem(), newItem());
        }
    }
}
  • 上面代码实现了“石头、剪刀、布”游戏,采用了接口的方式进行多路分发(两路分发),避免了需要判断类型的代码。

使用enum分发

enum Outcome { WIN, DRAW, LOSE}
enum RoShamBo2 {
    PAPER(Outcome.DRAW, Outcome.LOSE, Outcome.WIN),
    SCISSORS(Outcome.WIN, Outcome.DRAW, Outcome.LOSE),
    ROCK(Outcome.LOSE, Outcome.WIN, Outcome.DRAW);
    private Outcome vPaper, vScissors, vRock;
    RoShamBo2(Outcome paper, Outcome scissors, Outcome rock) {
        this.vPaper = paper;
        this.vScissorc = scissors;
        this.vRock = rock;
    }
    public Outcome compete(RoShamBo2 it) {
        switch(it) {
            default:
            case Outcome.PAPER: return vPaper;
            case Outcome.SCISSORS: return vScissors;
            case Outcome.ROCK: return vRock;
        }
    }
}
// 调用略
  • 上面代码使用构造器来初始化每个枚举实例,并以一组结果作为参数,形成类类似查询表的结构。

使用常量相关的方法

  • 为每个枚举实例提供方法的不同实现同样可以完成多路分发,但是在需要添加类型时代码的改动量要多于使用enum分发。
enum Outcome { WIN, DRAW, LOSE}
enum RoShamBo3 {
    PAPER {
        public Outcome compete(RoShamBo3 it) {
            switch(it) {
                default: 
                case PAPER: return DRAW;
                case SCISSORS: return LOSE;
                case ROCK: return WIN;
            }
        }
    };
    // SCISSORS, ROCK略 
}

使用EnumMap分发

enum Outcome { WIN, DRAW, LOSE}
enum RoShamBo5 {
    PAPER, SCISSORS, ROCK;
    static EnumMap<RoShamBo5, EnumMap<RoShamBo5, Outcome>> table = new EnumMap<>(RoShamBo5.class);
    static {
        for (RoShamBo5 it: RoShamBo5.values()) {
            table.put(it, new EnumMap<>(RoShamBo5.class));
        }
        initRow(PAPER, Outcome.DRAW, Outcome.LOSE, Outcome.WIN);
        initRow(SCISSORS, Outcome.WIN, Outcome.DRAW, Outcome.LOSE);
        initRow(ROCK, Outcome.LOSE, Outcome.WIN, Outcome.DRAW);
    }
    static void initRow(RoShamBo5 it, Outcome vPAPER, Outcome vSCISSORS, Outcome vROCK) {
        EnumMap<RoShamBo5, Outcome> row = RoShamBo5.table.get(it);
        row.put(RoShamBo5.PAPER, vPAPER);
        row.put(RoShamBo5.SCISSORS, vSCISSORS);
        row.put(RoShamBo5.ROCK, vROCK);
    }
    public Outcome compete(RoShamBo5 it) {
        return table.get(this).get(it);
    }
}
  • 使用EnumMap实现类似于表格的方法,从而达到了分发的目的。当然也可以使用二维数组来实现。

Java编程思想笔记0x11

枚举类型(一)

基本enum特性

  • 调用enumvalues()方法,可以遍历enum实例,返回一个enum实例的数组,并且数组中实例的顺序和声明时的顺序保持一致。
  • 创建enum时,编译器会为你生成一个相关的类,这个类继承自java.lang.Enum
  • ordinal()方法返回一个int值,表示每个enum实例在声明中的次序,从0开始计算。
  • 可以使用==来比较enum实例,编译器会自动提供equals()hashCode()方法。
  • Enum实现了Comparable接口,拥有compareTo()方法。
  • name()方法返回enum实例声明时的名字,和toString()方法相同。
  • valueOf()根据参数给出的实例名称返回相应的enum实例,如果不存在该名称的实例将会抛出异常。

enum是java中的语法糖,实际反编译后是继承自Enum类。

向enum中添加新方法

  • enum除了不能被继承,和普通的类基本相同。
public enum Main {
    WEST("west"),
    NORTH("north"),
    EAST("east"),
    SOUTH("south");
    private String desc;
    Main(String desc) {
        this.desc = desc;
    }
    public String getDesc() {
        return desc;
    }
    public static void main(String[] args) {
        for (Main m: Main.values()) {
            System.out.println(m + " " + m.getDesc());
        }
    }
}
/* Output:
WEST west
NORTH north
EAST east
SOUTH south
*/
  • 如果希望为enum添加方法,必须先定义enum实例,并且在实例序列最后添加一个分号。否则在编译时会得到错误消息。
  • 可以为enum添加构造方法,注意声明enum实例时就是在使用enum的构造方法。此外,enum的构造方法无所谓是否private,因为就算不声明private其构造方法也只能在enum内部使用。
  • 可以覆盖原来的toString()方法,以提供定制化效果。

enum的构造方法不能被反射调用,当反射调用构造方法时会检查对应类是否为枚举类。

switch语句中的enum

  • case语句中不需要enum类来修饰enum实例。

values()方法

// 反编译enum代码
final class Apple extends java.lang.Enum<Apple> {
  public static final Apple A;
  public static final Apple B;
  public static final Apple C;
  public static Apple[] values();
  public static Apple valueOf(java.lang.String);
  static {};
}
  • values()方法和valueOf(String)方法是编译器向enum中加入的静态方法。
  • 编译器将enum标记为final类,因此无法继承enum

随机选取

public class Enums {
    private static Random rand = new Random(42);
    public static <T extends Enum<T>> T random(Class<T> ec) {
        return random(ec.getEnumConstants());
    }
    public static <T> T random(T[] values) {
        return values[rand.nextInt(values.length)];
    }
}
  • <T extends Enum<T>>限制泛型必须为enum,使用Class<T>可以获取枚举的实例数组。

使用接口组织枚举

interface Food {
    enum Appetizer implements Food {
        SALAD, SOUP, SPRING_ROLLS
    }
    enum MainCourse implements Food {
        LASAGNE, BURRITO, PAD_THAI, LENTILS
    }
    enum Dessert implements Food {
        TIRAMISU, GELATO, BLACK_FOREST_CAKE
    }
    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, ESPRESSO
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Food f = Food.Appetizer.SALAD;
        f = Food.Coffee.BLACK_COFFEE;
        f = Food.Dessert.BLACK_FOREST_CAKE;
    }
}
  • 在一个接口的内部,创建实现该接口的枚举,以此将元素进行分组,可以达到将枚举元素分类组织的目的。