欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Effective Java学习笔记

程序员文章站 2022-07-26 17:15:31
Java学习笔记静态工厂方法静态工厂方法考虑使用静态工厂方法代替构造静态工厂方法与构造器不同的第一优势在于,它们有名字第二个优势,不用每次被调用时都创建新对象第三个优势,可以返回原返回类型的子类第四个优势,在创建带泛型的实例时,能使代码变得简洁(jdk1.8已经解决)除此之外可以有多个参数相同但名称不同的工厂方法可以减少对外暴露的属性多了一层控制,方便统一修改Java 中,获得一个类实例最简单的方法就是使用 new 关键字,通过构造函数来实现对象的创建。就像这样:Fragment...

静态工厂方法

考虑使用静态工厂方法代替构造
静态工厂方法与构造器不同的第一优势在于,它们有名字
第二个优势,不用每次被调用时都创建新对象
第三个优势,可以返回原返回类型的子类
第四个优势,在创建带泛型的实例时,能使代码变得简洁(jdk1.8已经解决)
除此之外
可以有多个参数相同但名称不同的工厂方法
可以减少对外暴露的属性
多了一层控制,方便统一修改

Java 中,获得一个类实例最简单的方法就是使用 new 关键字,通过构造函数来实现对象的创建。

就像这样:
Fragment fragment = new MyFragment();
// or
Date date = new Date();

不过在实际的开发中,我们经常还会见到另外一种获取类实例的方法:
Fragment fragment = MyFragment.newIntance();
// or
Calendar calendar = Calendar.getInstance();
// or
Integer number = Integer.valueOf(“3”);

↑ 像这样的:不通过 new,而是用一个静态方法来对外提供自身实例的方法,即为我们所说的静态工厂方法(Static factory method)。
惯用名称:
• from——类型转换方法,它接受单个参数并返回此类型的相应实例,例如:Dated=Date.from(instant);
• of ——聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:Set faceCards=EnumSet.of(JACK,QUEEN,KING);
• valueOf —— from 和 to 更为详细的替代方式,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
• instance或getinstance ——返回一个由其参数(如果有的话)描述的实例,但不能说它具有相同的值, 例如:StackWalkerluke=StackWalker.getInstance(options);
createnewInstance——与instance或getInstance类似,除此之外该方法保证每次调用返回一个新 的实例,例如:ObjectnewArray=Array.newInstance(classObject,arrayLen);
• getType ——与getInstance类似,但是在工厂方法处于不同的类中的时候使用。getType中的Type 是工厂方法返回的对象类型,例如:FileStorefs=Files.getFileStore(path);
• newType——与newInstance类似,但是在工厂方法处于不同的类中的时候使用。newType中的Type 是工厂方法返回的对象类型,例如:BufferedReaderbr=Files.newBufferedReader(path);
• type —— getType 和 newType 简洁的替代方式,例如:List litany = Collections.list(legacyLitany);

当构造参数过多时使用Builder模式

NutritionFactscocaCola=newNutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();
客户端调用 builder 对象的与 setter 相似方法来设置你 想设置的可选参数。最后,客户端调用 builder 对象的一个无参的 build 方法来生成对象,该对象通常是不可变 的。Builder 通常是它所构建的类的一个静态成员类。上述为利用 Builder 模式模拟实现的可选参数。未优化之前需要对所有参数传参或者写很多不同的排列组合形式的构造函数,优化后可以实现较方便的可选参数构造类对象。
Effective Java学习笔记
通过协变返回类型(重写方法的返回类型可以是重写方法返回类型的子类)可以使父类方法在子类中直接调用而不需要强制转换
另外还可以将多个子类调用的参数聚合到单个属性中(跟继承相关)
通常只有在参数数量四个及以上时才使用builder构建方法,并且出现这种情况时 为避免方法弃用带来的错误,最好第一次就直接用builder.

可以通过私有构造方法的方式使该类不能是实例化,而不要通过抽象类的方式,因为该类可以子类化,子类可以实例化

依赖注入优于硬连接资源

许多类依赖于一个或多个底层资源(对象实例),可以通过提供带参的构造函数注入资源,用户可以根据需要选择注入的资源类的具体实例,也可以通过spring的@autowire自动注入。
依赖注入也适用于构造方法、静态工厂和builder模式
静态实用类和单例类不适合于需要引用底层资源的类。

避免创建不必要的对象

尤其是对象不可变时,尽量重用对象而不要创建新的对象,避免资源浪费
适配器是一个隐形的对象不可变对象
优先使用基本类型而不是装箱基本类型(包装器),因为装箱基本类型

消除过期对象的引用

及时消除非活动元素的引用,置null
当缓存中某一项生面周期有key’决定而不是value决定时,使用weakhashmap,可以自动将过期的项删除

避免使用finalizer和cleaner机制

jvm的gc机制在什么时候被调用,被不被调用是不可控的;对于非final类,finalizer机制可以在静态属性记录对对象的引用,防止被垃圾收集,是一种finalizer机制攻击。

使用try-with-resources语句代替try-finally语句

try-with-resource的close方法不可见,try-with-resource避免了显示调用close时对try中的异常内容覆盖

重写equals方法时遵守通用约定

通常在值类(表示值的类)中才需要重写equals方法,否则需要满足五大要求才重写equals方法。
一般配合==和instanceof使用
重写equals必须重写hashcode方法

始终重写tostring方法

最好重写tostring就加上文档注释,以避免后期需要tostring格式更改带来的麻烦

谨慎重写clone方法

尽量不要去用cloneable接口重写clone的方法实现复制,尽量使用复制构造方法或工厂的方法实现复制功能

考虑实现comparable接口

实现coparato方法后,对象数组排序会更简单,尤其具有明显自然顺序(字母顺序、数字顺序、时间顺序)的值类,都应该实现comparable接口,被比较对象通常工行时同类型对象

  1. == : 基本数据类型都用这个比较,
    Java里面包含8个基本数据类型,分别是:
    boolean、byte、char、short、int、float、double、long
    注意String 可不属于基本数据类型,它是个类…

2…equals() 用于引用数据类型(除了上面的8个,都是引用数据类型,包括封装类,Object子类等), 比较是否指向相同的对象,
例如 String str = “abc”;
等效于: char data[] = {‘a’, ‘b’, ‘c’}; String str = new String(data);
就是str 指向了 new String(data) 这个对象. 如果再有其他变量也指向这个对象,他们比较的结果就返回true;
由于此方法在Object里, 所以它有很多重写, 具体要看说明;
另外```equalsIgnoreCase()可以忽略大小写;
Object和String的equals()实现存在区别,所以上面的例子不太友好。有demo在最后
3. compareTO 在基本数据中,compareTo()是比较两个Character 对象;string中可以比较两个字符串并且得到顺序.
按字典顺序比较两个字符串。该比较基于字符串中各个字符的 Unicode 值。将此 String 对象表示的字符序列与参数字符串所表示的字符序列进行比较。
==操作符并不涉及对象内容的比较。若要对对象内容进行比较,则用equals;==是对对象地址的比较,而equals是对对象内容的比较。对于基本数据类型,一般用==,而对于字符串的比较,一般用equals

2、对于compareTo(), 在API中,java.lang包下面的基本数据类型的封装类都提供了该方法,如 Integer,Float,Byte,Short,Character 等

使类和成员的可访问性最小化

很少用public修饰公共类的实例字段,因为线程不安全

在公共类中使用访问方法而不是公共属性

最小化可变性

不可变类:实例一但被创建,成员变量的值不可修改。实例不能被修改的类,实例中的所有信息在对象的生命周期中是固定的
使用不可变类设计更简单,实现使用更容易和安全
使类不可变:使用final private修饰,类不能被继承,不要提供修改对象状态的方法,确保对可变组件的互斥访问
防御性拷贝:一个类有从他的客户端得到或者返回的可变组件,那么这个类必须防御性的拷贝这些组件,使用拷贝的对象进行操作防止对原数据进行修改
如果不可变类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身
不可变对象线程安全,不需要同步

组合优于继承

不去继承一个类,而是将新类中增加私有属性属性为现有类的实例引用,现有类成为新类的组成部分,叫组合。
继承会不必要的公开实现细节,产生的api将与原始实现联系在一起,限制类的性能;暴露类内部,客户端可以直接访问,进而改变了底层实现破坏类子类不变性。

设计继承类相对辛苦,必须文档说明所有自用模式

接口优于抽象类

接口可以定义混合类型,所谓混合类型就是混合多个类的能力,产生的可以表示混型中所有类型的类。另外接口操作会更灵活,尤其面对非层级类型框架。
对于接口的公共方法,通过骨架实现类去实现,从而子类不需重复实现,骨架实现类作为一个抽象类再令需要实现接口的类去继承。

为后代设计接口

接口仅用来定义类型

不要去写用于导出常量的糟糕接口,想导出常量:
1.常量与现有类或接口密切相关,则直接将其添加到类或接口中;
2.使用枚举导出
2.用不可实例化的工具类导出

类层次结构优于标签类

不要将所有标签罗列在一个类中,然后多个实现混杂在一个类中,将标签类转换为类层次,将标签类拆分开,每个类型的实现由自己的类来实现

支持使用静态成员类而不是非静态类

四种嵌套类:静态成员类,非静态成员类,匿名类,局部类
静态成员类:仅在与外部类一起使用时才有用,如果嵌套类的实例可以与宿主类的实例隔离存在,那么嵌套类必须是静态成员类,因为不可能再没有宿主实例的情况下创建非静态成员类的实例,因此声明一个不需要访问宿主实例的成员类的话就加上static修饰符,使它成为静态成员类,避免宿主引用带来的时间和空间浪费
匿名类:适用于只存在一次,并且只为创建一个对象

将源文件限制为单个*类

一个源文件中有多个*类容易产生在其他源文件也有同名类时产生调用冲突,所以不要将多个*类或接口放在一个源文件中,从而编译时不会有多个定义,如果这些*类从属于同一个类,则把他们变为静态成员类,并且声明为私有来减少类的可访问性。

不要使用原始类型

使用泛型,如果不知道实际类型参数是什么,则使用无限制通配符<?>;Set<Object>是参数化类型,表示一个可以包含任何类型对象的集合,Set是一个原始类型,他不在反省类型系统之列。

消除非检查警告

尽可能消除每一个未经检查的警告(由泛型带来),不能消除的警告,如果确定代码安全,使用注解抑制警告(@SuppressWarnings("unchecked")),并且添加注释,为什么安全

列表优于数组

数组和泛型一般不能结合在一起用,例如创建泛型类型的数组(new List<E>[ ]),参数化类型的数组(new List<String>[])以及类型参数的数组(new E[])都是非法的,但是创建无限定通配符类型的数组是合法的,因为擦除机制,编译时可以唯一确定参数化类型。数组是协变和具体化的,泛型是不变的,类型可擦除的,如果将两者混合产生编译时错误或警告,首先想到使用列表List<T>替换数组T[]

优先考虑泛型

将类泛型化,泛型化类的第一步是在其声明中添加一个或多个类型参数,通常使用E作为类型参数表示堆栈的元素类型

优先使用泛型方法

形如: public static <E> Set<E> union(Set<E> S1,Set<E> S2)
限定类型:public static <E extends Comparable<E>> E max(Collection<E> c)

使用限定通配符来增加api的灵活性

<? extends E>
<? super E>

为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型,使用规则:PECS(producer-extends,consumer-super),作为生产者的参数化类型使用extends,作为消费者的参数化类型则使用super。
例如Comparable实例总是消费者,所以通常使用Comparable<? super T>优于Comparable<T>;Comparator也总是消费者,同理,Comparator<? super T>
tips:不要将限定通配符作为返回类型

合理的结合泛型和可变参数

尤其是在给另一个方法访问一个泛型可变参数数组的情况下,这种操作是不安全的,除非两种情况:1.确保数组传递给另一个可变参数方法是类型安全的,并用@SafeVarargs标注的;2.数组传递给一个非可变参数的方法是安全的,该方法仅计算数组内容的一些方法。

优先考虑类型安全异构容器

Favorites:允许客户端保存和检索任意多种类型的favorite实例,该类型的Class对象是参数化键的一部分,因为Class类是泛型的,也就是不是简单的Class,而是Class<T>,也称其为类型令牌,类型令牌无限制,如果需要有限制可以通过限定参数类型或限定通配符。Favorites实例是类型安全的,请求什么类型返回就是什么类型,他也是异构的,与Map不同,所有的键都是不同类型,所以被称为类型安全异构容器。
Class类中有asSubclass,是一种安全(动态)执行其参数表示的类的子类的类型转换实例方法。
安全异构容器使用场合:泛型API中限制了每个容器的固定数量的类型参数,此时有需要的话可以使用类型安全异构容器,他通过将类型参数放在键上而不是容器上没解决了此限制。

使用枚举类型代替整型常量

枚举类型允许添加任意方法和属性并实现任意接口,他们提供了所有Object方法的高质量实现,实现了Comparable和Serializable,不单单可以作为枚举常量的简单集合。
有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的方法,并用常量特定的类主体中的每个常量的具体方法重写它。这种方法被称为特定于常量(constant-specific)的方法实现:

//Enum type with constant specific method implementations 
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);
 }

策略枚举示例:(每次添加枚举常量时*选择加班费策略。幸运的是,有一个很好的方法来实现这一点。这 个想法是将加班费计算移入私有嵌套枚举中,并将此策略枚举的实例传递给 PayrollDay 枚举的构造方法。然 后,PayrollDay 枚举将加班工资计算委托给策略枚举,从而无需在 PayrollDay 中实现 switch 语句或特 定于常量的方法实现)

//The strategy enum pattern
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);
} 
}
}
 

使用实例属性代替序数

永远不要从枚举的序号中得出与他相关的值,因为不相关,将相关值保存在实例属性中
形如:

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; }
 }

传递常量集合时使用EnumSet替代位属性

EnumSet可以有效表示从单个枚举类型中提取的值集合,这个类实现了Set接口,在其内部,每个EnumSet都表示为一个位矢量,大多数情况下,整个EnumSet用单个long表示,所以他的属性与位属性的性能相当。
使用示例:

text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));

使用EnumMap替代序数索引

使用序数索引数组不知道序数与数组索引之间的关系,并且如果修改数组,此序数索引就无效了,而EnumMap不会有这个问题

使用接口模拟可扩展的枚举

枚举类型不可扩展,但接口类型是可以扩展的,所以可以两者配合,实现接口的基本枚举类型
示例:

public interface Operation{
 double apply(double x, double y);
}

public enum Basic Operation 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;
} 
}
   

注解优于命名模式

当能用注解代替时,尽量不用命名

始终使用Override注解

所有涉及到重写父类声明的方法声明上的都使用@Override,避免由于疏漏将重写变成重载。
使用注解后程序会利用检查功能抛出警告

使用标记接口定义类型

标记接口是不包含方法声明的接口,只是指定或标记一个类实现了那些属性的接口,标记接口定义了一个由标记类实例实现的类型。
如果标记是应用于除类或接口以外的 任何程序元素,则必须使用注解,因为只能使用类和接口来实现或扩展接口。如果标记仅适用于类和接口,则应该优先使用标记接 口而不是注解。

lambda表达式优于匿名类

Lambdas 在功能上与匿名类相似,但更为简洁。lambda 没有名称和文档; 如果计算不是自解释的,或者超过几行,则不要将其放入 lambda 表达式中。一行代码对于 lambda 说是理想的,三行代码是合理的最大值。
匿名类能做lambda无法做的:
1.Lambda 仅限于函数式接口。如果你想创建一个抽象类的实例,你可以使用匿名类来 实现,但不能使用 lambda
2.可以使用匿名类来创建具有多个抽象方法的接口实例
3.匿名类可以对自身引用(this),lambda不可以,如果需要从内部访问函数对象,必须使用匿名类

方法引用优于lambda表达式

使用方法引用通常会得到更短,更清晰的代码,lambda 变得太⻓或太复杂,它们也会给 你一个结果:你可以从 lambda 中提取代码到一个新的方法中,并用对该方法的引用代替 lambda。如果lambda更简洁的情况还是使用lambda

优先使用标准的函数式接口

一般来说,最好使用 java.util.function.Function 中提供的标准接口,如果没有一个标准模块能够满足您的需求,例如,如果需要一个带有三个参数的 Predicate,或者一个抛出检查异常的 Predicate,那么需要编写自己的代码,有时候你应该编写自己的函数式接口,即使与其中 一个标准的函数式接口的结构相同。设计自己的接口要小心,始终使用 @FunctionalInterface 注解标注你的函数式接口。

明智审慎使用Stream

Stream API,以简化顺序或并行执行批量操作的任务。该 API 提供了两个关键的抽象: 流 (Stream),表示有限或无限的数据元素序列,以及流管道 (stream pipeline),表示对这些元素的多级计算。 Stream 中的元素可以来自任何地方。常⻅的源包括集合,数组,文件,正则表达式模式匹配器,伪随机数生成器 和其他流。流中的数据元素可以是对象引用或基本类型。支持三种基本类型:int,long 和 double。
流管道由源流(source stream)的零或多个中间操作和一个终结操作组成。每个中间操作都以某种方式转 换流,例如将每个元素映射到该元素的函数或过滤掉所有不满足某些条件的元素。中间操作都将一个流转换为另 一个流,其元素类型可能与输入流相同或不同。终结操作对流执行最后一次中间操作产生的最终计算,例如将其 元素存储到集合中、返回某个元素或打印其所有元素。
管道延迟(lazily)计算求值:计算直到终结操作被调用后才开始,而为了完成终结操作而不需要的数据元 素永远不会被计算出来。这种延迟计算求值的方式使得可以使用无限流。请注意,没有终结操作的流管道是静默 无操作的,所以不要忘记包含一个。
Stream API 流式的(fluent):: 它设计允许所有组成管道的调用被链接到一个表达式中。事实上,多个管 道可以链接在一起形成一个表达式。
流可以很容易地做一些事情:
• 统一转换元素序列
• 过滤元素序列
• 使用单个操作组合元素序列 (例如添加、连接或计算最小值)
• 将元素序列累积到一个集合中,可能通过一些公共属性将它们分组 • 在元素序列中搜索满足某些条件的元素
如果使用这些技术最好地表达计算,那么使用流是这些场景很好的候选者。

1.应该避免使用流来处理 char 值。
2.过度使用流会使得程序难于阅读和维护,所以仅在有意义的情况下在新代码中使用他们

优先考虑流中无副作用的函数

收集器:可以看作是封装缩减策略(reduction strategy)的不透明对象,收集器生成的对象通常 是一个集合(它代表名称收集器)。
静态导入收集器的所有成员是一种惯例和明智的做 法,因为它使流管道更易于阅读。除了toList()、toSet() 和 toCollection(collectionFactory),它们分别返回集合、列表和程序员指定的集合类型,收集器中的其他 36 种方法呢?它们中的大多数都是用于将流收集到 map 中的,这比将流收集到真正的 集合中要复杂得多。每个流元素都与一个键和一个值相关联,多个流元素可以与同一个键相关联。
映射收集器:toMap(keyMapper、valueMapper)其中的参数为两个函数,第一个将流元素映射到键,第二个将流元素映射到值
三个参数的情况:
1.

Map<Artist, Album> topHits = albums.collect(toMap(Album::artist, a->a, maxBy(comparing(Album::sales)))); 

其中第二个参数a -> a表示选择将原来的对象作为map的value值,这个函数实现了「将专辑(albums)流转换为地map,将每位艺术家(artist)映射到销售量最佳的专辑。」
2.

 toMap(keyMapper, valueMapper, (oldVal, newVal) ->newVal) 

用于发生冲突时,此时第三个参数表示key值相同时,使用后边的新值覆盖前边的
toMap还有四个参数的形式,它是一个map工厂,用于指定特定的map实现,例 如EnumMap或TreeMap。

优先使用Collection而不是Stream来作为方法的返回类型

Collection接口是Iterable的子类型,并且具有stream方法,因此它既能提供迭代和又能提供流访问。因此, Collection或适当的子类型通常是公共序列返回方法的最佳返回类型。

谨慎使用流并行

Java5引入了java.util.concurrent类库,带有并发集合 和执行器框架。Java7引入了fork-join包,这是一个用于并行分解的高性能框架。Java8引入了流,可以通过 对parallel方法的单个调用来并行化。
通常,并行性带来的性能收益在ArrayList、HashMap、HashSet和ConcurrentHashMap实例、 数组、int类型范围和long类型的范围的流上最好。用于执行此任务的流类库使用的抽象是 spliterator,它由spliterator方法在Stream和Iterable上返回,其他情况谨慎使用流并行,并行化一个流不仅会导致糟糕的性能,包括活性失败(livenessfailures);它会导致不正确的结果和不可预 知的行为(安全故障)。
并行性的最佳终操作是缩减(reductions),即使用流的reduce方法组合管道中出现的所有元素,或者预先打包的reduce(如min、 max、count和sum)。短路操作anyMatch、allMatch和noneMatch也可以支持并行性。由Stream 的collect方法执行的操作,称为可变缩减(mutablereductions),不适合并行性,因为组合集合的开销非常 大。

检查参数有效性

索引值必须是非负数,对象 引用必须为非null。要清楚地在文档中记载所有这些限制,并在方法主体的开头用检查来强制执行,对于公共方法和受保护方法,请使用Java文档@throws注解来记在在违反参数值限制时将引发的异 常。
在Java7后,常用Objects.requireNonNull 执行空值检查,不需手动。
养成习惯,每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。应该记住这些限制,并在方法体的开头使用显式检查来强制执行这些限制。

必要时进行防御性拷贝

仔细设计方法签名

1.仔细选择方法名名称
2.不要过分地提供方便的方法,不要在类或接口中定义过多的方法,难以维护
3.避免过⻓的参数列表。目标是四个或更少的参数,尤其是参数类型一致时,更要避免
4.对于参数类型,优先选择接口而不是类
5.与布尔型参数相比,优先使用两个元素枚举类型

明智审慎地使用重载

重载方法的参数是在编译期决定的,所以不能动态的接收参数,一个安全和保守的策略是永远不要导出两个具有相同参数数量的重载,最好避免重载具有相同数量参数的多个签名的方法。尽量去使用不同的方法名称去替代重载。
对于构造方法,无法使用不同的名称:类的多个构造函数总是被重载。在许多情况下,可以选择导出静态工 厂而不是构造方法。

明智审慎地使用可变参数

在性能关键的情况下使用可变参数时要小心。每次调用可变参数方法都会导致数组分配和初始化。
在使用可变参数前加上任何必需的参数,避免没有参数的调用带来的运行时失败而不是编译时失败。

返回空的数组或集合,不要返回null

不要考虑分配空容器的开销,如果实在担心,可以通过重复返回相同的不可变空集合或零长度数组(不可变对象可以共享)来避免分配。

明智审慎地返回Optional

在Java8中,还有第三种方法来编写可能无法返回任何值的方法。Optional类表示一个不可变的容 器,它可以包含一个非null的T引用,也可以什么都不包含。不包含任何内容的Optional被称为空(empty)。 非空的包含值称的Optional被称为存在(present)。Optional的本质上是一个不可变的集合,最多可以容纳一 个元素。Optional没有实现Collection接口,但原则上是可以。
返回Optional的方法比抛出异常的方法更灵活、更容易使 用,而且比返回null的方法更不容易出错。 Optional.of(value)方法接受一个可能为null的值,如果传 入null则返回一个空的Optional。但是永远不要通过返回Optional的方法返回一个空值:它破坏Optional设计的 初衷。
Optional在本质上类似于检查异常 (checkedexceptions)(详⻅第71条),因为它们迫使API的用户面对可能没有返回任何值的事实。抛出未检查 的异常或返回null允许用户忽略这种可能性,从而带来潜在的可怕后果。但是,抛出一个检查异常需要在客户端 中添加额外的样板代码。
并不是所有的返回类型都能从Optional的处理中获益。容器类型,包括集合、映射、Stream、数组和 Optional,不应该封装在Optional中。与其返回一个空的Optional<List>,不还如返回一个空的 List。
如果可能无法返回结果,并且 在没有返回结果,客户端还必须执行特殊处理的情况下,则应声明返回Optional的方法。除了「次要基本类型(minor primitive types)」Boolean,Byte, Character,Short和Float之外,永远不应该返回装箱的基本类型的Optional。

为所有已公开的API元素编写文档注释

最小化局部变量的作用域

最小化局部变量作用域的最强大的技术是直到首次使用的地方再声明它,最终技术是保持方法小而集中。如果在同一方法中组合两个行为,则 与一个行为相关的局部变量可能会位于执行另一个行为的代码范围内。为了防止这种情况发生,只需将方法分为 两个:每个行为对应一个方法。

for-each循环优于传统for循环

有三种常⻅的情况是不能分别使用for-each循环,而要使用for循环的:
• 有损过滤(Destructivefiltering)——如果需要遍历集合,并删除指定选元素,则需要使用显式迭代器, 以便可以调用其remove方法。通常可以使用在Java8中添加的Collection类中的removeIf方法,来 避免显式遍历。
• 转换——如果需要遍历一个列表或数组并替换其元素的部分或全部值,那么需要列表迭代器或数组索引来 替换元素的值。
• 并行迭代——如果需要并行地遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或 索引变量都可以同步进行(正如上面错误的card和dice示例中无意中演示的那样)。
否则还是使用for-each循环

了解并使用库

若需要精确答案就应避免使用float和double类型

使用BigDecimal、int或long:
如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以 使用int或long。如果数值不超过9位小数,可以使用int;如果不超过18位,可以使用long。如果数量可能超 过18位,则使用BigDecimal。

基本数据类型优于包装类

当使用其他类型更合适时应避免使用字符串

如果使用不当,字符串比其他 类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。

当心字符串连接引起的性能问题

不要使用字符串连接操作符合并多个字符串,除非性能无关紧要。否则使用StringBuilder的 append方法。或者,使用字符数组,再或者一次只处理一个字符串,而不是组合它们。

接口优于反射

反射允许一个类使用另一个类,即使在编译前者时后者并不存在。然而,这种能力是有代价的:
• 你失去了编译时类型检查的所有好处,包括异常检查。如果一个程序试图反射性地调用一个不存在的或不 可访问的方法,它将在运行时失败,除非你采取了特殊的预防措施。
• 执行反射访问所需的代码既笨拙又冗⻓。写起来很乏味,读起来也很困难。
• 性能降低。反射方法调用比普通方法调用慢得多。到底慢了多少还很难说,因为有很多因素在起作用。在我 的机器上,调用一个没有输入参数和返回int类型的方法时,用反射执行要慢11倍。

明智审慎的使用本地方法

Java 本地接口(JNI)允许 Java 程序调用本地方法,这些方法是用 C 或 C++ 等本地编程语言编写的。从历史上 看,本地方法主要有三种用途。它们提供对特定于平台的设施(如注册中心)的访问。它们提供对现有本地代码库 的访问,包括提供对遗留数据访问。最后,本地方法可以通过本地语言编写应用程序中注重性能的部分,以提高性 能。
原因:1.现在大多数任务java中已经有了类似的功能,并且一般都比去访问本地库更快
2.本地方法不安全,容易受内存毁坏错误的影响,也更依赖于平台,可移植性差
3.本地代码需要与java代码“粘合”,增加了编写难度
如果必须使用本地方法访问底层资源或本地库,也尽可能少的使用本地代码

明智审慎的进行优化

所以在编写代码的时候就努力写好,设计系统时考虑性能,特别是在设计 API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。

遵守被广泛认可的命名约定

排版约定
包名和模块名应该是分层的,组件之间用句点分隔。组件应该由小写字母组成,很少使用数字。任何在你的组 织外部使用的包,名称都应该以你的组织的 Internet 域名开头,并将组件颠倒过来,例如,edu.cmu、com.google、 org.eff。以 java 和 javax 开头的标准库和可选包是这个规则的例外。用户不能创建名称以 java 或 javax 开头的 包或模块。
类和接口名称,包括枚举和注释类型名称,应该由一个或多个单词组成,每个单词的首字母大写,例如 List 或 FutureTask。除了缩略语和某些常⻅的缩略语,如 max 和 min,缩略语应该避免使用。方法或字段名的第一个字母是小写,其他和类和接口的排版约定相同。
常量字段」,它的名称应该由一个或多个大写单词组成,由下划线分隔。
类型参数名通常由单个字母组成。最常⻅的是以下五种类型之一:T 表示任意类型,E 表示集合的元素类型, K 和 V 表示 Map 的键和值类型,X 表示异常。函数的返回类型通常为 R。任意类型的序列可以是 T、U、V 或 T1、T2、T3。
示例:
Effective Java学习笔记
语法命名
包没有语法命名约定。可实例化的类,包括枚举类型,通 常使用一个或多个名词短语来命名,例如 Thread、PriorityQueue 或 ChessPiece。不可实例化的实用程序类 (详⻅第 4 条)通常使用复数名词来命名,例如 collector 或 Collections。接口的名称类似于类,例如集合或 比较器,或者以 able 或 ible 结尾的形容词,例如 Runnable、Iterable 或 Accessible。因为注解类型有很多的用途,所以没有哪部分占主导地位。名词、动词、介词和形容词都很常⻅,例如,BindingAnnotation、Inject、 ImplementedBy 或 Singleton。
执行某些操作的方法通常用动词或动词短语(包括对象)命名,例如,append 或 drawImage。返回布尔值 的方法的名称通常以单词 is 或 has(通常很少用)开头,后面跟一个名词、一个名词短语,或者任何用作形容词 的单词或短语,例如 isDigit、isProbablePrime、isEmpty、isEnabled 或 hasSiblings。
返回被调用对象的非布尔函数或属性的方法通常使用以 get 开头的名词、名词短语或动词短语来命名,例如 size、hashCode 或 getTime。
转换对象类型(返回不同类型的独立对象)的实例方法通常称为 toType,例 如 toString 或 toArray。返回与接收对象类型不同的视图(详⻅第 6 条)的方法通常称为 asType,例如 asList。 返回与调用它们的对象具有相同值的基本类型的方法通常称为类型值,例如 intValue。静态工厂的常⻅名称包括 from、of、valueOf、instance、getInstance、newInstance、getType 和 newType。

只针对异常的情况下才使用异常

异常永远不应该用于正常的程序控制流程。

对可恢复的情况使用受检异常,对编程错误使用运行时异常

Java 程序设计语言提供了三种 throwable:受检异常(checked exceptions)、运行时异常(runtime exceptions) 和错误(errors)。
如果期望调用者能够合理的恢复程序运行,对于这 种情况就应该使用受检异常。通过抛出受检异常,强迫调用者在一个 catch 子句中处理该异常,或者把它传播出 去。
有两种非受检的 throwable:运行时异常和错误。在行为上两者是等同的:它们都是不需要也不应该被捕获 的 throwable。如果程序抛出非受检异常或者错误,往往属于不可恢复的情形,程序继续执行下去有害无益。如 果程序没有捕捉到这样的 throwable,将会导致当前线程中断(halt),并且出现适当的错误消息。
用运行时异常来表明编程错误。实现的所有非受检的 throwable 都应该是 RuntimeExceptiond 子类(直接或者间接的)。不仅不应该定义 Error 的子类,也不应该抛出 AssertionError 异常。
对于可恢复的情况,要抛出受检异常;对于程序错误,就要抛出运行时异常。不确定是否可恢复, 就跑出为受检异常。不要定义任何既不是受检异常也不是运行异常的抛出类型。要在受检异常上提供方法,以便 协助程序恢复。

避免不必要的使用受检异常

如果调用者无法恢复失败,就应该抛出未受检异常;如果可以恢复,并且想要迫使调用者处理异常的条 件,首选应该返回一个 optional 值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。

优先使用标准的异常

1。最经常被重用的标准异常类型是 IllegalArgumentException,当调用者传递的参数值不 合适的时候,往往就会抛出这个异常。
2.另一个经常被重用的异常是 llegalStateException。如果因为接收对象的状态而使调用非法,通常就 会抛出这个异常。例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
3.另一个值得了解的通用异常是 ConcurrentModificationException。如果检测到一个专⻔设计用 于单线程的对象,或者与外部同步机制配合使用的对象正在(或已经)被并发地修改,就应该抛出这个异常。
4.最后一个值得注意的标准异常是 UnsupportedOperationException。如果对象不支持所请求的操 作,就会抛出这个异常。很少用到它,因为绝大多数对象都会支持它们实现的所有方法。

抛出与抽象对应的异常

如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,只有在低层方法的规范 碰巧可以保证“它所抛出的所有异常对于更高层也是合适的”情况下,才可以将异常从低层传播到高层。异常链对 高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析。

每个方法抛出的异常都需要创建文档

始终要单独地声明受检异常,并且利用 Javadoc 的 @throws 标签,准确地记录下抛出每个异常的条件。
如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这 是可以接受的,而不是为每个方法单独建立文档。

Effective Java学习笔记

异常包含详细信息

为了捕获失败, 异常的细节信息应该包含“对该异常有 用”的所有参数和字段的值,但是千万不要在细节消息中包含密码、密钥以及类似的信息!
为了确保在异常的细节消息中包含足够的失败-捕捉信息,一种办法是在异常的构造器而不是字符串细 节消息中引人这些信息。然后,有了这些信息,只要把它们放到消息描述中,就可以自动产生细节消息。
示例:

/**  
 * Constructs an IndexOutOfBoundsException.
 * @param lowerBound the lowest legal index value
 * @param upperBound the highest legal index value plus one
 * @param index the actual index value
 */
public IndexOutOfBoundsException( int lowerBound, 
int upperBound, int index ) {
    // Generate a detail message that captures the failure
super(String.format(
"Lower bound: %d, Upper bound: %d, Index: %d", lowerBound, upperBound, index ) );
    // Save failure information for programmatic access
this.lowerBound = lowerBound; 
this.upperBound = upperBound; 
this.index = index;
}

保持失败原子性

失败原子性:失败的方法调用应该使对象保持在被调用之前的状态。
实现途径:
1.设计一个不可变的对象。
2.对于可变对象,在执行操作之前检查参数的有效性, 使得在对象的状态被修改之前,先抛出适当的异常。或者调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。
3.在对象的一份临时拷⻉上执行操作,当操作完成之后再用临时拷⻉中的结 果代替对象的内容。
4.编写一段恢复代码(recovery code),由它来拦截操作过程中发生的失败,以及便对象回滚到操作开始之前的状态上。这种办法主要用于永久性的(基于磁盘的) 数据结构。

不要忽略异常

不要写空的catch块,即使是选择忽略异常,catch 块中也要应该包含一条注释,说明为什么可以这么做,并且变量应该命名为 ignored。

并发

同步访问共享的可变数据

同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块 的每个线程,都能看到由同一个锁保护的之前所有的修改效果。
最佳办法是不共享可变的数据。要么共享不可变的数据,要么压根不共享。换句话说,将可变数据限制在单个线程中。当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。
tips:合理使用synchronized、 volatile 修饰符和使用 AtomicLong 类。AtomicLong 类是java.util.concurrent.atomic 的组成部分。这个包为在单个变量上进行免锁定、线程安全的编程提供了基本类型,比volatile还多了原子性。

避免过度同步

同步方法中不要调用外来方法,如可重写的方法,观察者方法,因为这些方法可能产生异常,死锁等问题导致同步失败。
应该在同步区域内做尽可能少的工作。就尽量只把获得锁,检查共享数据,根据需要转换数据,然后释放锁这些放在同步范围。 如果要执行某个很耗时的动作,则应该设法把这个动作移到同步区域的外面。

executor、task和stream优先于线程

为特殊的应用程序选择executor service,如果是编写小程序或轻量负载的服务器,使用Executors.newCachedThreadPool,他不需要配置;在大负载的产品服务器中,最好使用 Executors.newFixedThreadPool ,它提供了一个包含固 定线程数目的线程池,或者为了最大限度地控制它,就直接使用 ThreadPoolExecutor 类。
尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。当直接使用线程时,Thread 是既充当工作单元,又是执行机制。而在 Executor Framework 中,工作单元和执行机制是分开的。
任务(task)有两种:Runnable 及其近亲 Callable (它与 Runnable 类似,但它会返 回值,并且能够抛出任意的异常)。执行任务的通用机制是 executor service.

优先使用并发工具类(java.util.concurrent包中的类)而不是wait/notify

同步器(Synchronizer)是使线程能够等待另一个线程的对象,允许它们协调动作,最常用的同步器是CountDownLatch (倒计数锁存器)和 Semaphore 。
如果非要使用wait/notify,要使用wait循环模式调用wait方法,不要在循环外调用wait方法;为了唤醒正在等待的线程,应该是中使用notifyall方法。只有处于等待状态的所有线程都在等待同一个条件,而每次只有一个线程可以从这个条 件中被唤醒时,那么你就应该选择调用 notify 方法,而不是 notifyAll 方法。

文档应包含线程安全属性

线程安全级别
1.不可变的—这个类的实例看起来是常量。不需要外部同步。
2.无条件线程安全 — 该类的实例是可变的,但是该类具有足够的内部同步,因此无需任何外部同步即可并发 地使用该类的实例。例如 AtomicLong 和 ConcurrentHashMap。
3.有条件的线程安全 — 与无条件线程安全类似,只是有些方法需要外部同步才能安全并发使用。示例包括 Collections.synchronized 包装器返回的集合,其迭代器需要外部同步。
4.非线程安全 — 该类的实例是可变的。要并发地使用它们,客户端必须使用外部同步来包围每个方法调用 (或调用序列)。这样的例子包括通用的集合实现,例如 ArrayList 和 HashMap。
5.线程对立 — 即使每个方法调用都被外部同步包围,该类对于并发使用也是不安全的。线程对立通常是由于 在不同步的情况下修改静态数据而导致的。没有人故意编写线程对立类;此类通常是由于没有考虑并发性而 导致的。当发现类或方法与线程不相容时,通常将其修复或弃用。
每个类都应该措辞严谨的描述或使用线程安全注解清楚地记录其线程安全属性。synchronized 修 饰符在文档中没有任何作用。有条件的线程安全类必须记录哪些方法调用序列需要外部同步,以及在执行这些序 列时需要获取哪些锁。如果你编写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保 护你免受客户端和子类的同步干扰,并为你提供更大的灵活性,以便在后续的版本中采用复杂的并发控制方式。

明智审慎的使用延迟初始化

延迟初始化有它的用途。如果一个字段只在类的一小部分实例*问,并且初始化该字段的代价很高,那 么延迟初始化可能是值得的。唯一确定的方法是以使用和不使用延迟初始化的效果对比来度量类的性能。
其他情况,大多都是常规初始化优于延迟初始化。
如果使用延迟初始化来取代初始化的循环(circularity),请使用同步访问器(synchronized);如果需要在静态字段上使用延迟初始化来提高性能,使用 lazy initialization holder class 模式;如果需要使用延迟初始化来提高实例字段的性能,请使用双重检查模式。这个模式背后的思想是两次检查字段的值(因此得名 double check):一次没有 锁定,然后,如果字段没有初始化,第二次使用锁定。只有当第二次检查指示字段未初始化时,调用才初始化字段。 由于初始化字段后没有锁定,因此将字段声明为 volatile 非常重要。

不要依赖线程调度器

不要依赖 Thread.yield 或线程优先级。这些工具只是对调度器的提示。

序列化

优先选择Java序列化的替代方案

Java原生序列化
Java原生序列化使用ObjectInputStream.readObjectObjectOutputStream.writeObject来序列化和反序列化对象。对象必须实现Serializable接口,同时对象也可以自实现序列化方法readObject和反序列化方法writeObject定义对象成员变量的序列化和反序列化方式。此外对象还可实现writeReplace,readResolve,serialVersionUID
writeReplace主要用来定义对象具体序列化的内容,可对原来按照readObject方法序列化的内容进行修改替换。
readResolve主要用来定义对象反序列化时的内容,可对原来按照writeObject方法反序列化的内容进行修改替换。
serialVersionUID是类的序列化版本号,保证能将二进制流反序列化为内存中存在的类的对象。如果不主动生成的话,在序列化反序列化过程中根据类信息动态生成,耗时且类结构不灵活。
序列化是危险的,应该避免。如果你从头开始设计一个系统,可以使用跨平台的结构化数据,如 JSON 或 protobuf。不要反序列化不可信的数据。如果必须这样做,请使用对象反序列化过滤,但要注意,它不能保证 阻止所有攻击。避免编写可序列化的类。如果你必须这样做,一定要非常小心。

非常谨慎地实现Serializable

实现 Serializable 接口的代价是
1.一旦类的实现被发布,它就会降低更改该类实现的灵活性。
2.增加产生bug和安全漏洞的风险。
3.增加新版本发布的测试负担。
为继承而设计的类很少情况适合实现 Serializable 接口,接口也很少情况适合扩展它;内部类不应该实现 Serializable;但是,静态成员类可以实现 Serializable 接口。

考虑使用自定义序列化的格式

只有在合理描述对象的逻辑状态时,才使用默认的序列化形式;否则,设计一个适合描述对象的自定义 序列化形式。设计类的序列化形式应该和设计导出方法花的时间应该一样多,都应该严谨对待。

保护性的实现readObject方法

当一个对象被反序列化的 时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷⻉,这是非常重要的。

对于实例控制,枚举类型优于readResolve

任何一个 readObject 方法,不管是显式的还是默认的,都 会返回一个新建的实例,这个新建的实例不同于类初始化时创建的实例。
如果依赖readResolve方法实现单例的话,需要做很多额外工作,如将所有成员变量用transient修饰等,此外仍然会拥有Java序列化的缺点。
而枚举类对象的序列化和反序列化方式是Java语言规范的,不是由用户实现的。枚举类对象是天生的单例对象。

考虑用序列化代理代替序列化实例

序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的逻辑状 态。这个嵌套类被称为序列化代理(seralization proxy),它应该有一个单独的构造器,其参数类型就是那个外围 类。这个构造器只是从它的参数中复制数据:它不需要进行任何一致性检验或者保护性拷⻉。从设计的⻆度看,序 列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现 Serializable 接口。
也就是自实现writeReplace方法,好处是安全。
序列化代理模式有两个局限性。它不能与可以被客户端拓展的类兼容(详⻅ 19 条)。它也不能与对象图中包 含循环的某些类兼容:如果你企图从一个对象的序列化代理的 readResovle 方法内部调用这个对象的方法,就会 得到一个 ClassCastException 异常,因为你还没有这个对象,只有它的序列化代理

本文地址:https://blog.csdn.net/weixin_43877653/article/details/107347784

相关标签: Java学习 java