throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
… // Remainder omitted
}
上面代码虽然强制令period 实例的开始时间小于结束时间。然而,Date 类是可变的,很容易违反这个约束:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
从Java 8 开始,解决此问题的显而易⻅的方法是使用 Instant
(或LocalDateTime
或 ZonedDateTime
)代替Date
,因为他们是不可变的。但Date
在老代码里仍有使用的地方,为了保护 Period 实例的内部不受这种攻击,可以使用拷⻉来做 Period 实例的组件:
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(this.start + " after " + this.end);
}
有了新的构造方法后,前面的攻击将不会对Period 实例产生影响。注意:保护性拷⻉是在检查参数的有效性之前进行的,且有效性检查是在拷贝实例上进行的。
这样做可以避免从检查参数开始到拷贝参数之间的时间段内,其他的线程改变类的参数
也被称作 Time-Of-Check / Time-Of-Use 或 TOCTOU攻击
看了之前章节的同学可能有疑问了,这里为什么没用clone
方法来进行保护性拷贝?
答案是:Date
不是final的,所以clone
方法不能保证返回类确实是 java.util.Date
的对象,也可能返回一个恶意的子类实例。
但是普通方法就不一样了,它们在进行保护性拷贝是允许使用clone
方法,原因是我们知道Period
内部的Date
对象类型确实是java.util.Date
。
对于参数类型可能被恶意子类化的参数,不要使用 clone
方法进行防御性拷⻉。
其实,改变Period
实例仍是有可能的:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
修改方法也很简单:
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
上面的分析带来的启发是:应该尽量使用不可变对象作为对象内部的组件,这样就不必担心保护性拷⻉。在 Period
示例中,使用Instant
(或LocalDateTime
或ZonedDateTime
)。另一个选项是存储Date.getTime()
返回的long类型来代替Date引用。
最后,如果拷贝成本较大的话,并且我们新人使用它的客户端不会恶意修改组件,则可以在文档中指明客户端不得修改受到影响的组件,以此来代替保护性拷贝。
这一条介绍了若干经验:
1. 谨慎给方法起名
-
方法名应该选易于理解的,并且与同一个包里其他名称的风格一致
-
选择大众认可的名称
2. 不要过于追求提供便利的方法
方法太多会使类难以学习、使用、文档化、维护。只有当一项操作被经常用到时,才考虑为它提供快捷方式(shorthand)
3. 避免过长的参数列表,相同类型的长参数序列格外有害
参数个数不超过4个
有三种技巧可以缩短过长的参数列表:
-
把一个方法分解成多个方法,每个方法只需要这些参数的一个子集。例如:
java.util.List
接口里没有提供在子列表中查找元素的第一个索引和最后一个索引的方法。相反,它提供了subList
方法,返回子列表。此方法可以与indexOf
或lastIndexOf
方法结合使用来达到所需的功能。 -
创建辅助类用来保存参数的分组。例如:编写一个表示纸牌游戏的类,发现需要两个参数来表示纸牌的点数和花色,这时就可以创建一个类来表示卡片。
-
从对象构建到方法调用全都采用
Builder
模式
4. 优先使用接口作为入参类型
只要有适当的接口可用来定义参数,就优先使用这个接口,而不是使用实现该接口的类。例如:在编写方法时使用Map
接口作为参数
5. 对于boolean型参数,优先使用有两个元素的枚举
例如,有一个 Thermometer
类型的静态工厂方法,这个方法的签名需要以下这个枚举的值:
public enum TemperatureScale { FAHRENHEIT, CELSIUS }
Thermometer.newInstance(TemperatureScale.CELSIUS)
不仅比Thermometer.newInstance(true)
更有意义,而且可以在将来的版本中将新的枚举值添加到 TemperatureScale
中,而无需向 Thermometer
添加新的静态工厂。
下面这个程序试图将一个集合进行分类:
public class CollectionClassifier {
public static String classify(Set<?> s) {
return “Set”;
}
public static String classify(List<?> lst) {
return “List”;
}
public static String classify(Collection<?> c) {
return “Unknown Collection”;
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet(),
new ArrayList(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections)
System.out.println(classify©);
}
}
运行结果是打印了三次Unknown Collection。为什么会这样呢?
原因就是classify
方法被重载了,要调用哪个重载方法是在编译时做出决定的。for循环里参数的编译时类型一直是Collection<?>
,所以唯一适合的重载方法是classify(Collection<?> c)
有一个很有意思的事实:重载(overloaded)方法的选择是静态的,重写(overridden)方法的选择是动态的。
重写方法的选择是在运行时进行的,依据是被调用的方法所在的对象的运行时类型。
以下面这个例子具体说明:
class Wine {
String name() {
return “wine”;
}
}
class SparklingWine extends Wine {
@Override
String name() {
return “sparkling wine”;
}
}
class Champagne extends SparklingWine {
@Override
String name() {
return “champagne”;
}
}
public class Overriding {
public static void main(String[] args) {
List wineList = Arrays.asList(
new Wine(), new SparklingWine(), new Champagne());
for (Wine wine : wineList)
System.out.println(wine.name());
}
}
这段代码打印出wine,sparkling wine和champagne,尽管在每次迭代里,实例的编译类型都是Wine,但总是会执行最具体(most specific)
的重写方法,也就是在子类上调用的就执行被子类覆盖的方法。
在CollectionClassifier
示例中,程序的目的是根据参数的运行时类型自动执行适当的方法重载来辨别参数的类型。但方法重载完全没有提供这样的功能,这段代码最佳修改方案是:用单个方法来替换这三个重载的classify
方法,代码逻辑里用instanceof
判断:
public static String classify(Collection<?> c) {
return c instanceof Set ? “Set” : c instanceof List ? “List” : “Unknown Collection”;
}
如果API的普通用户根本不知道哪个重载会被调用,使用这样的API就会报错。所以,应该避免混淆使用重载。
安全保守的策略是:一个安全和保守的策略是永远不要编写两个具有相同参数数量的重载。
因为我们始终可以给方法起不同的名字,避免使用重载。
例如,考虑ObjectOutputStream
类。对于每个类型,它的write方法都有一种变体,例如writeBoolean(boolean)
、writeInt(int)
和writeLong(long)
。这种命名模式的另一个好处是,可以为read方法提供相应的名称,例如readBoolean()
、readInt()
和readLong()
。
一个类的多个构造器总是重载的,可以选择导出静态工厂。
对于每一对重载方法,至少要有一个形参在这两个重载中具有「完全不同的」类型。这时主要的混淆根源就没有了。例如ArrayList
有接受int的构造方法和接受Collection
的构造方法。
Java有一个自动装箱的概念,他们的出现也引入了一些麻烦:
public class SetList {
public static void main(String[] args) {
Set set = new TreeSet<>();
List list = new ArrayList<>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
System.out.println(set + " " + list);
}
}
实际上,程序从Set
中删除非负值,从List
中删除奇数值,并打印 [-3, -2, -1] 和 [-2, 0, 2]。
-
set.remove(i)
选择重载了remove(E)
方法,执行结果正确 -
list.remove(i)
的调用选择重载remove(int i)
方法,它将删除列表中指定位置的元素,所以最终打印 [-2, 0, 2]
有两种手段可以解决这个问题:
-
强制转换
list.remove
的参数为Integer -
调用
Integer.valueOf(i)
,将结果传递list.remove
方法
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove((Integer) i); // or remove(Integer.valueOf(i))
}
在Java 8中添加Lambda表达式和方法引用以后,进一步增加了重载混淆的可能性。
new Thread(System.out::println).start();
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
Thread 构造方法调用和submit方法调用看起来很相似,但是前者编译而后者不编译。参数是相同的(System.out::println
)。因为sumbit
方法有一个带有Callable <T>
参数的重载,而Thread构造方法却没有。在submit这里不知道应该调用哪个方法。
在更新现有类时,可能会违反这一条目中的指导原则。例如,从Java 4开始就有一个contentEquals(StringBuffer)
方法。在Java 5中,添加了contentEquals(CharSequence)
接口。但只要这两个方法返回相同的结果就可以,例如下面的代码:
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence) sb);
}
原因是这两个重载互相调用。
Java类库在很大程度上遵循了这一条中的建议,但是有一些类违反了它。例如,String导出两个重载的静态工厂方法valueOf(char[])
和valueOf(Object)
,这应该被看成是一种反常行为。
可变参数方法接受0个或多个指定类型的参数,首先创建一个数组,其大小是在调用位置传递的参数数量,然后将参数值放入数组中,最后将数组传递给方法。
例如,这里有一个可变参数方法,返回入参的总和:
static int sum(int… args) {
int sum = 0;
for (int arg : args)
sum += arg;
return sum;
}
有时,编写一个需要某种类型的一个或多个参数的方法是合适的,而不是0个或者多个。可以在运行时检查数组⻓
度:
static int min(int… args) {
if (args.length == 0)
throw new IllegalArgumentException(“Too few arguments”);
int min = args[0];
for (int i = 1; i < args.length; i++)
if (args[i] < min)
min = args[i];
return min;
}
最严重的是,如果客户端在没有参数的情况下调用此方法,则它在运行时而不是在编译时失败。
有一种更好的方法可以达到预期的效果。声明方法采用两个参数,一个指定类型的普通参数,另一个此类型的可变参数。
static int min(int firstArg, int… remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs)
if (arg < min)
min = arg;
return min;
}
在性能关键的情况下使用可变参数时要小心。每次调用可变参数方法都会导致数组分配和初始化。
还有一种模式可以让你如愿以偿:
public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
public void foo(int a1, int a2, int a3, int… rest) { }
当参数数目超过3个时需要创建数组。
EnumSet
类的静态工厂使用这种方法,将创建枚举集合的成本降到最低。
像如下的方法并不罕⻅:
private final List cheesesInStock = …;
/**
-
@return a list containing all of the cheeses in the shop,
-
or null if no cheeses are available for purchase.
*/
public List getCheeses() {
return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
把没有奶酪(Cheese)可买的情况当做一种特例,这是不合常理的。这样需要在客户端中必须有额外的代码来处理null的返回值:
List cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
System.out.println(“Jolly good, just the thing.”);
这样做很容易出错,因为编写客户端的程序员可能忘记编写特殊情况代码来处理null返回。
下面是返回可能为空的集合的典型代码。一般情况下,这些都是必须的:
public List getCheeses() {
return new ArrayList<>(cheesesInStock);
}
如果有证据表明分配空集合会损害性能,可以通过重复返回相同的不可变空集合来避免多次分配
// Optimization - avoids allocating empty collections
public List getCheeses() {
return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheesesInStock);
}
数组的情况与集合的情况相同。永远不要返回null,而是返回⻓度为零的数组。
// Optimization - avoids allocating empty arrays
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
在Java 8之前,编写在特定情况下无法返回任何值的方法时,可以采用两种方法。要么抛出异常,要么返回null。这两种方式都不完美:
-
抛出异常代价很高,因为在创建异常时捕获整个堆栈trace
-
返回null有可能抛
NullPointerException
异常
在Java 8中,还有第三种方法来编写可能无法返回任何值的方法。Optional<T>
类表示一个不可变的容器,它可以包含一个非null的T引用,也可以什么都不包含。
不包含任何内容的Optional被称为空(empty)。非空的包含值称的Optional被称为存在(present)
返回Optional的方法比抛出异常的方法更灵活、更容易使用,而且比返回null的方法更不容易出错。
例如在第30条中,有一个根据集合中元素的自然顺序计算集合最大值的方法:
public static <E extends Comparable> E max(Collection c) {
if (c.isEmpty())
throw new IllegalArgumentException(“Empty collection”);
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return result;
}
如果给定集合为空,此方法将抛出IllegalArgumentException
异常。更好的替代方法是返回Optional<E>
。下面是修改后的方法:
public static <E extends Comparable>
Optional max(Collection c) {
if (c.isEmpty())
return Optional.empty();
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return Optional.of(result);
}
将null传递给Optional.of(value)
是一个编程错误,会抛NullPointerException
异常。Optional.ofNullable(value)
方法接受一个可能为null的值,如果传入null则返回一个空的Optional
。
Stream
上的很多终止操作返回Optional
。可以用Stream
重写max
方法,Stream
的max
操作会为我们生成Optional
的工作:
public static <E extends Comparable> Optional max(Collection c) {
return c.stream().max(Comparator.naturalOrder());
}
如果方法返回一个Optional,则客户端可以选择在方法无法返回值时要采取的操作。有以下两种方式:
1. 指定默认值
String lastWordInLexicon = max(words).orElse(“No words…”);
2. 抛出任何适当的异常
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
注意,我们传递的是异常工厂,而不是实际的异常。这避免了创建异常的开销
有时候,可能会遇到这样一种情况:获取默认值的代价很高,我们希望避免这种代价。对于这些情况,Optional
提供了一个方法orElseGet
,传入一个Supplier<T>
。
Optional
还提供了isPresent()
方法,可以将其视为安全阀。如果Optional
包含值,则返回true;否则返回false。
当使用Stream时,经常会遇到Stream<Optional<T>>
,为了推动进程还需要一个包含了非空optional中所有元素的Stream<T>
。Java 8里可以这样写:
streamOfOptionals
.filter(Optional::isPresent)
.map(Optional::get)
并不是所有的返回类型都能从Optional的处理中获益。容器类型,包括集合、映射、Stream、数组和Optional,不应该封装在Optional中。与其返回一个空的Optional<List<T>>
,不还如返回一个空的List<T>
。
那么什么时候应该声明一个方法来返回Optional<T>
而不是T 呢?
如果可能无法返回结果,并且在没有返回结果,客户端还必须执行特殊处理的情况下,则应声明返回Optional <T>
的方法。
使用Optional
还有一些其他的注意事项:
-
永远不应该返回基本包装类型的Optional(小型的
Boolean
,Byte
,Character
,Short
和Float
除外) -
不适合将optional作为键、值、集合或数组中的元素
-
除了作为返回值之外,不要在任何其他地方中使用
Optional
如果API要可用,就必须对其编写文档化。
要正确地记录API,必须在每个导出的类、接口、构造方法、方法和属性声明之前加上文档注释。如果一个类是可序列化的,应该对它的序列化形式编写文档。puiblic类不应该使用无参构造方法,因为无法为它们提供文档注释。要编写可维护的代码,还应该为所有没有被导出的类、接口、构造方法、方法和属性编写文档注释,尽管这些注释不需要像导出API元素那样完整。
方法的文档注释应该简洁地描述方法与其客户端之间的约定。这个约定应该说明方法做了什么,而不是它如何完成工作的。文档注释应该列举方法的所有前置条件以及后置条件
前置条件:为了使客户能够调用这个方法,必须要满足的条件
后置条件:调用完成之后,哪些条件必须满足
通常,每个未受检的异常都对应一个前提违例(precondition violation),要在受影响的参数的 @param
标签中指定前置条件
方法还应在文档中记录它的副作用(side effort)。例如,如果方法启动后台线程,则应该在文档里说明。
文档注释应该为每个参数都有一个 @param
标签,一个 @return
标签,以及一个 @throw
标签。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/ac0f7ddc27c395ff02ad14d0370fcbe8.jpeg)
最后
分享一些系统的面试题,大家可以拿去刷一刷,准备面试涨薪。
这些面试题相对应的技术点:
- JVM
- MySQL
- Mybatis
- MongoDB
- Redis
- Spring
- Spring boot
- Spring cloud
- Kafka
- RabbitMQ
- Nginx
- …
大类就是:
- Java基础
- 数据结构与算法
- 并发编程
- 数据库
- 设计模式
- 微服务
- 消息中间件
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**[外链图片转存中…(img-P7whV2CA-1713663664566)]
[外链图片转存中…(img-xZ0VbboS-1713663664566)]
[外链图片转存中…(img-jhRTVh5W-1713663664567)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/ac0f7ddc27c395ff02ad14d0370fcbe8.jpeg)
最后
分享一些系统的面试题,大家可以拿去刷一刷,准备面试涨薪。
这些面试题相对应的技术点:
- JVM
- MySQL
- Mybatis
- MongoDB
- Redis
- Spring
- Spring boot
- Spring cloud
- Kafka
- RabbitMQ
- Nginx
- …
大类就是:
- Java基础
- 数据结构与算法
- 并发编程
- 数据库
- 设计模式
- 微服务
- 消息中间件
[外链图片转存中…(img-HSU1Locv-1713663664567)]
[外链图片转存中…(img-gldyaa7B-1713663664567)]
[外链图片转存中…(img-1oKTkcDG-1713663664567)]
[外链图片转存中…(img-aUEkFXg4-1713663664568)]
[外链图片转存中…(img-AvkDbh50-1713663664568)]
[外链图片转存中…(img-A7IPjAOm-1713663664568)]
[外链图片转存中…(img-qSrOrk5p-1713663664568)]
[外链图片转存中…(img-ZC7edulR-1713663664568)]
[外链图片转存中…(img-bX3bv2I8-1713663664569)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!