Java方法的设计
文章目录
一、检查参数的有效性
大多数方法和构造器对于传递给它们的参数值都会有某些限制。例如,索引值必须是非负,等等。
应该在文档中清楚地指明这些限制,并且在方法体的开头处检查参数,以强制施加这些限制。
- 对于共有的和受保护的方法,要用Javadoc的@throws标签(tag)在文档中说明违反参数值限制时会抛出的异常;
- 在Java7中增加的
Objects.requireNonNull
方法比较灵活且方便,因此不必手工进行null检查; - 在Java9中增加了检查范围的设施:
java.util.Objects
。这个设施包含了三个方法:checkFromIndexSize
、checkFromToIndex
、checkIndex
。这个设施不像检查null的方法那么灵活。它不允许指定自己的异常情况,而是专门设计用于列表和数组索引的。 - 非公有的方法通常应该使用断言(assertion)来检查它们的参数;
检查构造器参数的有效性是非常重要的,这样可以避免构造出来的对象违反了这个类的约束条件。
-
在方法执行它的计算任务之前,应该xian检查它的参数,这一规则也有例外。
一个重要的例外是,在某些情况下,有效性检查工作非常昂贵,或者根本是不切实际的,而且有效性检查已隐含在计算过程中完成。
二、必要时进行保护性拷贝
假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序。
-
Date已经过时了,不应该在新代码中使用;
因为Date类本身是可变的,从Java8开始,使用Instant(或LocalDateTime,或者ZonedDateTime)代替Date,因为Instant(以及另一个java.time类)是不可变的)。
-
对于构造函数的每个可变参数进行保护性拷贝是必要的;
-
在构造函数中,对于参数类型可以被不可信任放子类化的参数,请不要使用clone方法进行保护性拷贝;
访问方法与构造器不同,它们在进行保护性拷贝的时候允许使用clone方法。
-
保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查时针对拷贝之后的对象,而不是针对原始的对象;
-
长度非零的数组总是可变的。因此,在把内部数组返回给客户端之前,总要进行保护性拷贝。
另一个种解决方案是,给客户端返回数组的不可变试图:
private static final Thing[] private_values = {...}; private static final List<Thing> values = Collections.unmodifiableList(Arrays.asList(private_values));
三、谨慎设计方法签名
-
谨慎地选择方法的名称;
-
不要过于追求提供便利的方法;
-
避免过长的参数列表
(1) 把一个方法分解成多个方法;
(2)创建辅助类,用来保存参数的分组,然后将这个辅助类作用参数;
(3)从对象构建到方法调用都采用Builder模式;
-
对于参数类型,要优先使用接口而不是类;
-
对于boolean类型,要优先使用两个元素的枚举类型;
四、慎用重载
-
调用哪个重载方法是在编译时做出决定的
对于下面的代码,classify方法被重载了。每次迭代的运行时类型都是不同的,但这并不影响对重载方法的选择。因为该参数的编译时类型为
Collection<?>
,所以,唯一合适的重载方法是classify(Collection<?> lst)
,在循环的每次迭代中,都会调用这个重载方法。public class CollectionClassifier { public static String classify(Set<?> s){ return "Set"; } public static String classify(List<?> lst){ return "List"; } public static String classify(Collection<?> lst){ return "Unknown Collection"; } public static void main(String[] args) { Collection<?>[] collections = { new HashSet<String>(), new ArrayList<BigInteger>(), new HashMap<String, String>().values() }; // 将打印三次 “Unknown Collection” for (Collection<?> collection : collections) { System.out.println(classify(collection)); } } }
-
对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的
选择被覆盖的方法的正确版本是在运行时进行的,选择的依据是被调用方法所在对象的运行时类型。
如果实例方法在子类中被覆盖了,并且这个方法是在该子类的实例上被调用的,那么子类中的覆盖方法将会执行,而不管该子类实例的编译时子类到底是什么。
例如:下面的程序中,尽管在循环的每次迭代中,实例的编译时类型都为wine,当调用被覆盖的方法时,对象的编译时类型不会影响到哪个方法将被执行;
“最为具体”那个覆盖版本总是会得到执行。
/** * @Date 2020/2/12 * @Author lifei */ public class Overriding { public static void main(String[] args) { List<Wine> wineList = List.of( new Wine(),new SparklingWine(),new Champagne()); // 程序打印出 wine、sparkling wine、champagne for (Wine wine : wineList) { System.out.println(wine.name()); } } } 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"; } }
-
对于构造器,你没有选择使用不同名称的机会:一个类的多个构造器总是重载的
对于构造器,不用担心重载荷覆盖的相互影响,因为构造器不可能被覆盖。
在许多情况下,可以选择导出静态工厂,而不是构造器。
不会感到混淆的情形:
至少有一个对应的参数在两个重载方法中具有“根本不同”的类型,就属于这种不会感到混淆的情形。
-
在java 5 发行版本之前,所有的基本类型都根本不同于所有的引用类型,但是当自动装箱出现之后,就不在如此了
例如:下面的代码中,
set.remove(i)
调用的是boolean remove(Object o)
方法,list.remove(i)
调用的是重载的E remove(int index)
方法,因此,结果打印为:[-3, -2, -1] [-2, 0, 2]。将
list.remove(i)
改为list.remove(Integer.valueOf(i))
,结果打印为:[-3, -2, -1] [-3, -2, -1]/** * @Date 2020/2/12 * @Author lifei */ public class SetList { public static void main(String[] args) { Set<Integer> set = new TreeSet<>(); List<Integer> 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); //list.remove(Integer.valueOf(i)); } System.out.println(set + " " + list); } }
Java语言中添加了泛型和自动装箱之后,破坏了List接口。幸运的是,Java类库中几乎再没有API收到同样的破坏。
自动装箱和泛型成了Java语言的组成部分之后,慎用重载更加重要了。
-
在Java8 中增加了lambda和方法引用之后,进一步增加了重载造成混淆的可能。
// 可以正常编译 new Thread(System.out::println).start(); ExecutorService exec = Executors.newCachedThreadPool(); // 无法编译 exec.submit(System.out::println);
不要在相同的参数位置调用带有不同函数接口的方法。
-
"能够重载方法"并不意味着就“应该重载方法”;
-
同一组参数只需经过类型转换就可以被传递给不同的重载方法;
五、可变参数的使用
5.1 可变参数的工作机制
可变参数机制首先会创建一个数组,数组的大小为调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法。
printf
和反射机制都从可变参数中获得了极大的益处
可变参数是为
printf
而设计的,该方法是与可变参数同时添加到Java平台中的,为了核心的反射机制,被改造成利用可变参数。
5.2 在使用可变参数之前,要先包含所有必要的参数
优化之前:
public 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;
}
优化之后:
public static int min(int firstArg, int... remainingArgs){
int min = firstArg;
for (int arg: remainingArgs){
if (arg< min){
min = arg;
}
}
return min;
}
5.3 关注使用可变参数所带来的性能影响
使用可变参数性能优化的案例:
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){}
六、返回零长度的数组或者集合,而不是null
不要这样做:
private final List<Cheese> cheesesInStock = new ArrayList<>();
// 不要这样做
@Deprecated
public List<Cheese> getCheeses(){
return cheesesInStock.isEmpty()? null: new ArrayList<>(cheesesInStock);
}
-
返回null,会要求客户端重必须有额外的代码来处理null返回值
这样做容易出错,因为编写客户端程序的程序员会忘记写这种专门的代码来处理null返回值。
-
返回null而不是零长度的容器,也会使返回该容器的方法实现代码变得更复杂
6.1 创建不可变的零长度集合(这是一个优化,几乎用不上)
这种观点是站不住脚的:
有时会有人认为:null返回值比零长度集合或者数组更好,因为它避免了分配零长度的容器所需要的开销。
原因有两点:
第一,在这个级别上担心性能问题是不明智的,除非分析表明这个方法正是造成性能问题的真正源头;
第二,不需要分配零长度的集合或数组,也可以返回它们。
万一有证据表明分配零长度的集合损害了程序的性能,可以通过重复返回同一个不可变的零长度集合,避免了分配的执行,因为不可变对象可以被自由共享,下面的代码正是这么做的:
Collections.emptyList(); // 返回列表
Collections.emptySet(); // 返回集合
Collections.emptyMap(); // 返回映射
6.2 所有零长度的数组都是不可变的
如果确信分配零长度的数组会损害性能,可以重复返回同一个零长度的数组。
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheesesArray(){
// return cheesesInStock.toArray(new Cheese[0]);
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
七、Optional<T>
的使用
7.1 Optional<T>
的定义
Optional<T>
类代表的是一个不可变的容器,它可以存放单个非null的T引用,或者什么内容都没有。
- 不包含任何内容都option称为空(empty);
- 非空的option中的值称作存在(present);
optional本质上是一个不可变的集合,最多只能存放一个元素
Optional<T>
没有实现Collection<T>
接口,但原则上是可以的。
7.2 Optional
的用法
理论上能返回T的方法,实践中也可能无法返回,因此在某些特定的条件下,可以改为声明返回Optional<T>
.
返回Optional的方法比抛出异常的方法使用起来更灵活,并且比返回null的方法更不容易出错。
-
Optional.empty()
返回一个空的optional; -
Optional.of(value)
返回一个包含了指定非null值的optional;将null传入
Optional.of(value)
, 是一个编程错误。如果这么做,该方法会抛出NullPointerException
-
Optional.ofNullable(value)
方法接受可能为null的值当传入null值时就返回一个空的optional
永远不要通过返回Optional的方法返回null,因为它彻底违背了optional的本意。
-
Optional.empty().get()
将抛出异常:NoSuchElementException
因此当optional可能为空的时候,可以提供一个缺省值:
-
orElse(defaultValue)
设置一个默认值collection.stream().max(Comparator.naturalOrder()).orElse("default str");
-
orElseThrow(exceptionSupplier)
设置一个异常注意,此处传入的是一个异常工厂,而不是真正的异常。这避免了创建异常的开销,除非它真正抛出异常。
collection.stream().max(Comparator.naturalOrder()).orElseThrow(RuntimeException::new)
-
orElseGet(supplier)
collection.stream().max(Comparator.naturalOrder()).orElseGet(()->"aa");
-
filter
、map
、flatMap
、ifPresent
,还有java9新增的两个or
和ifPresentOrElse
-
isPresent()
可以当作一个安全阀当optional中包含一个值时,他返回true;当optional为空时,返回false。
该方法可以用于optional结果执行任意的处理,但要确保正确使用。
7.3 容器类型包括集合、映射、Stream、数组和optional,都不应该被包装在optional中
-
不要返回空的
Optional<List<T>>
,而应该只返回一个空的List<T>
;返回空的容器可以让客户端免与处理一个optional。
7.4 何时使用Optional<T>
规则是:如果无法返回结果并且当没有返回结果时客户端必须执行特殊的处理,那么就应该声明该方法返回Optional<T>
。
-
optional不适用于一些注重性能的情况
Optional 是一个必须进行分配和初始化的对象,从optional读取值时需要额外的开销。
-
永远不应该返回基本包装类型的optional,"小型的基本类型“(Boolean、Byte、Character、Short和Float)除外
返回一个包含了基本包装类型的optional,比返回一个基本类型的开销更高,因为optional有两级包装,不是0级。
类库设计师为基本类型(int、long和double)提供类似
Optional<T>
方法:OptionalInt
、OptionalLong
、OptionalDouble
-
几乎永远都不适合用optional作为键、值、或者集合或数组中的元素
-
不要将optional用作返回值以外的任何其他用途
八、编写文档注释
使用没有文档注释的API是非常痛苦的。
- 为了正确地编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前添加一个文档注释;
- 为了编写可维护的代码,还应该为那些没有被导出的类、接口、构造器、方法和域编写文档注释;
8.1 方法的注释
方法的文档注释应该简洁地描述出它和客户之间的约定。
除了专门为继承而设计的类中的方法之外,这个约定应该说明这个方法做了什么,而不是说明它是如何完成这项工作的。
(1)文档注释应该列举这个方法的所有前提条件和后置条件:
前提条件是指为了使客户能够调用这个方法,而必须要满足的条件;
后置条件是指在调用成功完成之后,那些条件必须要满足。
一般情况下,前提条件是由@throws
标签对未受检的异常所隐含描述的;
每个未受检的异常都对应一个前提违例。
也可以在一些受影响的参数的@param标记中指定前提条件。
(2)除了前提条件和后置条件之外,每个方法还应该在文档中描述它的副作用。
所谓副作用是指系统状态中可以观察到的变化,它不是为了获得后置条件而明确要求的变化。
例如方法启动了后台县城,文档中就应该说明这一点。
(3)方法的文档注释应该让每个参数都有一个@param
标签,以及一个@return
标签(除非这个方法的返回类型为void)
@param
标签或者@return
标签后面的文字应该是一个名词短语,描述了这个参数或者返回值所表示的值。在极少数情况下,也会用算术表达式来代替名次短语。
(4)对于该方法抛出的每个异常,无论是受检的还是未受检的都应有一个@throws
标签
跟在@throws
标签之后的文字应该包含单词if
,紧接着是一个名词短语,它描述了这个异常将在什么样的条件下抛出。
按照惯例,@param
、@return
、@throws
标签后面的短语或者子句都不用句点结束
(5){@code}
标签,以及<pre></pre>
{@code}
标签有两个作用:造成该代码片段以代码字体(code font)呈现;- 限制HTML标记和嵌套的Java标签在代码片段中进行处理;
在多行代码实例前使用字符<pre>{@code
,然后在代码后面加上}</pre>
,这样就可以在代码中保留换行,不需要对HTML元字符进行转义
(6)按惯例,当“this”一词被用在实例方法的文档注释中时,它应该始终是指方法调用所在的对象。
8.2 专门为了继承设计的类的文档编写
(1)必须在文档中注释它的自用模式
这些自用模式应该利用Java8中增加的@implSpec
标签进行文档注释
8.3 {@literal}
标签用于产生包含HTML元字符的文档
8.4 概要描述
每个文档注释的第一句话为该注释所在元素的概要描述。
概要描述必须独立地描述目标元素的功能,为了避免混淆,同一个类或者接口中的两个成员或者构造器,不应该具有同样的概要描述。
概要描述会在后面紧接着的空格、挑格或者行终结符的第一个句点处结束。
最好的解决方法是,用{@literal}
标签将讨厌的句点以及所有关联的文本都包起来。
8.5 为泛型、枚举、注解编写文档注释
- 当为泛型或者方法编写文档时,确保要在文档中说明所有的类型参数;
- 当为枚举编写文档时,要确保在文档中说明常量,以及类型;
- 为注解类型编写文档时,要确保在文档中说明所有成员,以及类型本身;
8.6 类或者静态方法是否线程安全,应该在文档中对它的线程安全级别进行说明
8.7 Javadoc具有“继承”方法注释的能力
如果一个API元素没有文档注释,Javadoc将会搜索最为适合的文档注释,接口的文档注释将优先于超类的文档注释。
也可以利用{@inheritDoc}
标签从超类型中继承文档注释的部分内容。
这意味着类还可以重用它所实现的接口的文档注释,而不需要拷贝这些注释。
更详细的文档注释规范:《The Java API Documentation Generator》