最后
整理的这些资料希望对Java开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
其实面试这一块早在第一个说的25大面试专题就全都有的。以上提及的这些全部的面试+学习的各种笔记资料,我这差不多来回搞了三个多月,收集整理真的很不容易,其中还有很多自己的一些知识总结。正是因为很麻烦,所以对以上这些学习复习资料感兴趣,
public enum Operation {
PLUS {
public double apply(double x, double y) {
return x + y;
}
},
MINUS {
public double apply(double x, double y) {
return x - y;
}
},
TIMES {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
public double apply(double x, double y) {
return x / y;
}
};
public abstract double apply(double x, double y);
}
特定于常量的方法实现可以与特定于常量的数据结合使用。toString 方法返回与操作关联的符号:
public enum Operation {
PLUS(“+”) {
public double apply(double x, double y) {
return x + y;
}
},
MINUS(“-”) {
public double apply(double x, double y) {
return x - y;
}
},
TIMES(“*”) {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE(“/”) {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public abstract double apply(double x, double y);
}
上述代码可以很容易地打印算术表达式:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf(“%f %s %f = %f%n”, x, op, y, op.apply(x, y));
}
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000
特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得更加困难。例如,考虑用一个枚举代表工资包中的工作天数,根据给定的某工人的基本工资(每小时)和当天工作的时间计算当天工人的工资。
enum PayrollDay {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate; // 计算基本工资
// 计算加班工资
int overtimePay;
switch (this) {
case SATURDAY:
case SUNDAY: // Weekend
overtimePay = basePay / 2;
break;
default: // Weekday
overtimePay = minutesWorked <= MINS_PER_SHIFT ?
0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
假设你给枚举添加了一个元素,可能是一个特殊的值来表示一个假期,但忘记在switch 语句中添加一个相应的case 条件。该程序仍然会编译,但付费方法会将节假日的工资算成工作日的工资,原因是走了上面default里的逻辑。
我们真正想要的是每次添加枚举常量时,就自动选择加班费策略:再定义一个嵌套枚举类PayType
,并将PayType
实例传递给·PayrollDay·枚举的构造方法里。
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
PayrollDay() {
this(PayType.WEEKDAY);
} // Default
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// The strategy enum type
private enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 :
(minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
枚举的switch语句适合于给外部的枚举类型增加和常量值对应行为。
假设希望 Operation
枚举有一个实例方法来返回每个相反的操作。
public static Operation inverse(Operation op) {
switch(op) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new AssertionError("Unknown op: " + op);
}
}
许多枚举天生就与某个 int 值关联。所以枚举都有一个ordinal
方法,返回每个枚举常量在类型中的数字位置。
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal() + 1;
}
}
这段代码维护起来就是一场噩梦,如果上面常量的顺序变了,所有用到numberOfMusicians
的地方都会返回一个不同的值。所以永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例属性中:
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}
实际上,ordinal
的目的是用于基于枚举的通用数据结构,如 EnumSet
和 EnumMap
,除了在编写这种数据结构时可以用,其他时候都不要用。
EnumSet 类集位属性的简介和性能优势及枚举类型的所有优点于一身。
如果枚举类型的元素主要用于集合中,一般就使用int枚举模式,例如将2的不同倍数设置成常量:
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
// Parameter is bitwise OR of zero or more STYLE_ constants
public void applyStyles(int styles) { … }
}
这种int型表示方式允许使用按位或(or)运算,将几个常量合并到一个称为位属性(bit field)的集合中:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
除了使用 OR 运算符之外,位属性表示法还可以支持求交集、异或求加法等很方便的操作,但是它本质上还是int型枚举常量,所以继承了int枚举常量的所有缺点:
1. 打印位属性时,翻译位域要难得多
就好比让你直接用二进制编程,酸爽程度可想而知
2. 没有一个好的方法可以遍历所有位属性表示的元素
3. 写API之前就要确定好需要多少位,选择相应的类型(int、long)
java.util
包提供了 EnumSet
类来有效地表示从单个枚举类型中提取的值集合。
在内部具体的实现上,每个EnumSet
表示为位矢量。
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set
}
EnumSet
提供了丰富的静态工厂,可以轻松创建集合:
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
因为枚举类型要用在集合中,所以不推荐用位域来表示它,推荐使用EnumSet
不要用ordinal来索引数组,要使用EnumMap
有时可能会用到ordinal
方法来作为数组的索引,例如创建一个植物类:
public class Plant {
enum LifeCycle {ANNUAL, PERENNIAL, BIENNIAL}
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
如果有一个植物数组plantsByLifeCycle
,按照不同的生长周期(一年生,多年生,或双年生)将花园里的植物garden
放入不同的位置:
Set[] plantsByLifeCycle = (Set[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++)
plantsByLifeCycle[i] = new HashSet<>();
for (Plant p : garden)
plantsByLifeCycle[p.lifeCycle.ordinal()].add§;
// Print the results
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf(“%s: %s%n”, Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
上面的代码有很多问题:
-
数组不能与泛型兼容,编译会报错
-
数组不知道它的索引代表什么,比如添加额外的注释来标注这些索引的输出
-
当使用以索引顺序为索引的数组时,必须人工保证使用的int值不出差错
使用java.util.EnumMap可以更好地达到上面想要的效果,并规避风险:
Map<Plant.LifeCycle, Set> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
plantsByLifeCycle.get(p.lifeCycle).add§;
System.out.println(plantsByLifeCycle);
EnumMap 与序数索引数组的速度相当,其原因正是 EnumMap 内部使用了这样一个数组,但它对程序员的隐藏了这个实现细节
注意EnumMap 构造方法传入一个Class对象:这是一个有限定的类型令牌(bounded type token),它提供运行时的泛型类型信息
通过使用stream可以进一步缩短程序:
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)));
这个代码的问题在于它选择了自己的 Map 实现,实际上并不是EnumMap
,使用Collectors.groupingBy
的三个参数形式的方法,它允许调用者使用mapFactory参数指定map的实现:
System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));
还有按照序数进行二维数组索引的情况,例如下面代码表示了物理学中物质的状态变化过程(如液体到固体是凝固,液体到气体是沸腾):
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// Rows indexed by from-ordinal, cols by to-ordinal
private static final Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME},
{FREEZE, null, BOIL},
{DEPOSIT, CONDENSE, null}
};
// Returns the phase transition from one phase to another
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}
程序看起来很优雅,但是和上面示例一样,有几个缺陷:
-
编译器不知道序数和数组索引之间的关系
-
如果在转换表中出错或者在修改Phase 或Phase.Transition枚举类型时忘记更新它,就会报错
按照上面的思路,可以用EnumMap
修改,使用Map(起始状态, Map(目标状态, 过渡方式))
这种存储格式:
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
.collect(groupingBy(t -> t.from, () -> new EnumMap<>(Phase.class),
toMap(t -> t.to, t -> t,
(x, y) -> y, () -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
映射的类型是Map<Phase, Map<Phase, Transition>>
,例如Map<液体, Map<固体, 凝固过程>>
第一个集合Phase
对Transition
进行分组,第二个集合使用从Phase
到Transition
的映射创建一个EnumMap
。第二个收集器((x, y)-> y))
中的merge
方法未使用,只有在我们因为我们想要获得一个EnumMap
而定义映射工厂时才需要用到
现在假设想为系统添加一个新阶段:plasma(离子)或电离气体。只有两个Transition
与之关联:电离化(ionization),将气体转为离子;和去离子;消电离化(deionization)将离子体转为气体。
要更新层序时,只需将PLASMA
添加到Phase
中,并将IONIZE(GAS, PLASMA)
和DEIONIZE(PLASMA, GAS)
添加到Transition
中:
public enum Phase {
SOLID, LIQUID, GAS, PLASMA;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),
IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
… // Remainder unchanged
}
}
很方便,也很安全!
对于“可继承”的枚举来说,操作码(operation codes, opcodes)是一个经典的例子,操作码是枚举类型,其元素表示某些机器上的操作,例如34中的 Operation 类型,它表示简单计算器上的功能。
有时需要让API来继承枚举,从而实现扩展功能的目的,但这种语法是不支持的,但可以通过接口的形式来巧妙地实现:
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS(“+”) {
public double apply(double x, double y) { return x + y; }
},
MINUS(“-”) {
public double apply(double x, double y) { return x - y; }
},
TIMES(“*”) {
public double apply(double x, double y) { return x * y; }
},
DIVIDE(“/”) {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
虽然枚举类BasicOperation
无法被继承,但接口Operation
是可以被继承的。假设想要扩展前面的操作类型,包括指数运算和余数运算。要做的就是编写一个实现 Operation
接口的枚举类型:
public enum ExtendedOperation implements Operation {
EXP(“^”) {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER(“%”) {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
测试程序如下:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum & Operation> void test(
Class opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants())
System.out.printf(“%f %s %f = %f%n”, x, op, y, op.apply(x, y));
}
注意ExtendedOperation
类的字面文字(ExtendedOperation.class)被传递给了test方法,<T extends Enum<T> & Operation> Class<T>
确保了Class 对象既是枚举又是Operation 的子类,这正是遍历元素和执行每个元素相关联的操作时所需要的
另一种方法是传入一个Collection<? extends Operation>
,和上面的差异在于这是一个限定通配符类型(⻅第31条),而不是传递了一个class 对象:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void test(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet)
System.out.printf(“%f %s %f = %f%n”, x, op, y, op.apply(x, y));
}
上面两个main函数的执行结果全都如下:
4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000
使用接口来实现可扩展枚举的一个小缺点是,无法实现一个枚举类继承另一个枚举类。
Java 平台也借鉴了这种方式来实现java.nio.file.LinkOption
枚举类型,它同时实现了 CopyOption
和 OpenOption
接口。
所有的程序员都应该使用Java平台所提供的预定义的注解类型,既然有了注解,就不要使用命名模式了
使用命名模式(naming patterns)来指示某些程序元素需要通过工具或框架进行特殊处理。例如在 JUnit 4 发布之前,要求程序员必须以test作为测试方法名的开头,这有几个严重缺点:
1. 英语拼写错误会导致运行失败,且没有任何提示
2. 无法实现将测试用于某个程序元素上
意思是,假如将TestSafetyMechanisms
类,希望JUnit 3 能够自动测试其所有方法,实际上并不会测试
3. 没有提供将参数值与程序元素相关联的好的方法
例如无法测试只有抛出异常才算成功的代码
注解很好地解决了所有这些问题,JUnit从版本4开始,使用注解来指定简单的测试:
import java.lang.annotation.*;
/**
-
Indicates that the annotated method is a test method.
-
Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
Test 本身还被 Retention 和 Target 这两个元注解进行标记,@Retention(RetentionPolicy.RUNTIME)
元注解表明Test这个注解在运行时有效,@Target.get(ElementType.METHOD)
元注解表明Test 注解只能修饰方法。
在Test 的注释说:“Use only on parameterless static method”(只用于无参的静态方法),实际上编译器并未强制限定这一条。
public class Sample {
@Test
public static void ml() {
} // Test should pass
public static void m2() {
}
@Test
public static void m3() { // Test should fail
throw new RuntimeException (“Boom”);
}
public static void m4() {
}
@Test
public void m5() {
} // INVALID USE: nonstatic method
public static void m6() {
}
@Test
public static void m7() { // Test should fail
throw new RuntimeException(“Crash”);
}
public static void m8() {
}
}
Sample 类有七个静态方法,其中四个被标注为Test。其中两个,m3 和m7 引发异常,两个m1 和m5 不引发异常。但是没有引发异常的注解方法之一是实例方法,因此它不是注释的有效用法。总之,Sample 包含四个测试:一个会通过,两个会失败,一个是无效的。未使用Test 注解标注的四种方法将被测试工具忽略。
注解永远不会改变被注解代码的语义,但是它可以通过工具进行特殊处理:
import java.lang.reflect.*;
import org.junit.Test;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) { //
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf(“Passed: %d, Failed: %d%n”, passed, tests - passed);
}
}
上述代码通过调用 Method.invoke
来反射地运行所有类标记有Test 注解的方法,运行结果:
现在添加对 仅在特定异常时 才算成功的测试的支持,添加一个新的注解类型:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
最后如何让自己一步步成为技术专家
说句实话,如果一个打工人不想提升自己,那便没有工作的意义,毕竟大家也没有到养老的年龄。
当你的技术在一步步贴近阿里p7水平的时候,毫无疑问你的薪资肯定会涨,同时你能学到更多更深的技术,交结到更厉害的大牛。
推荐一份Java架构之路必备的学习笔记,内容相当全面!!!
成年人的世界没有容易二字,前段时间刷抖音看到一个程序员连着加班两星期到半夜2点的视频。在这个行业若想要拿高薪除了提高硬实力别无他法。
你知道吗?现在有的应届生实习薪资都已经赶超开发5年的程序员了,实习薪资26K,30K,你没有紧迫感吗?做了这么多年还不如一个应届生,真的非常尴尬!
进了这个行业就不要把没时间学习当借口,这个行业就是要不断学习,不然就只能被裁员。所以,抓紧时间投资自己,多学点技术,眼前困难,往后轻松!
【关注】+【转发】+【点赞】支持我!创作不易!
lass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) { //
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf(“Passed: %d, Failed: %d%n”, passed, tests - passed);
}
}
上述代码通过调用 Method.invoke
来反射地运行所有类标记有Test 注解的方法,运行结果:
现在添加对 仅在特定异常时 才算成功的测试的支持,添加一个新的注解类型:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
最后如何让自己一步步成为技术专家
说句实话,如果一个打工人不想提升自己,那便没有工作的意义,毕竟大家也没有到养老的年龄。
当你的技术在一步步贴近阿里p7水平的时候,毫无疑问你的薪资肯定会涨,同时你能学到更多更深的技术,交结到更厉害的大牛。
推荐一份Java架构之路必备的学习笔记,内容相当全面!!!
[外链图片转存中…(img-RSkSXkWl-1714887930638)]
成年人的世界没有容易二字,前段时间刷抖音看到一个程序员连着加班两星期到半夜2点的视频。在这个行业若想要拿高薪除了提高硬实力别无他法。
你知道吗?现在有的应届生实习薪资都已经赶超开发5年的程序员了,实习薪资26K,30K,你没有紧迫感吗?做了这么多年还不如一个应届生,真的非常尴尬!
进了这个行业就不要把没时间学习当借口,这个行业就是要不断学习,不然就只能被裁员。所以,抓紧时间投资自己,多学点技术,眼前困难,往后轻松!
【关注】+【转发】+【点赞】支持我!创作不易!