1.创建与销毁对象
1.1考虑用静态工厂方法代替构造器
这里的静态工厂不是我们说设计模式中静态工厂方法模式;可以理解为对外提供的一种服务,不过是设置为静态的,方便我们调用
用静态工厂方法代替构造器的优点:
- 有名称,可以让调用者更加明确会产生什么类型的对象
- 结合final关键字,缓存等手段,可以每次都返回同一个对象,这样有助于提高性能
- 可以返回原返回类型的各种任何子类型对象:例如Collections Framework API(详见第52条)
- 利用服务提供者框架(有服务接口,服务提供者,服务调用者三个角色)
比如说:JDBC中,Connection就是服务接口,DriverManager.registerDriver是服务提供者API,DriverManager.getConnection(),是服务访问API
/**
* 提供服务
* @author lanyangjia
* @date 2019年4月1日21:29:03
*
*/
public interface Provider {
Service newService();
}
/**
* 服务注册接口
* @author lanyangjia
* @date 2019年4月1日21:29:11
*
*/
public interface Service {
void provideService();
}
/**
* 注册服务
* @author lanyangjia
* @date 2019年4月1日21:30:29
*
*/
public class Services {
//私有化构造函数
private Services(){}
//服务的集合
public static final Map<String,Provider> providers = new ConcurrentHashMap<>();
//默认服务提供者的名称
public static final String DEFAULT_PROVIDER_NAME = "<def>";
//注册服务提供者注册的默认API
public static void registerDefaultProvider(Provider p) {
registerProvider(DEFAULT_PROVIDER_NAME,p);
}
//提供参数的服务注册API
public static void registerProvider(String name, Provider p) {
providers.put(name,p);
}
/**调用服务**/
//调用Service的API
public static Service newInstance() throws IllegalAccessException {
return newInstance(DEFAULT_PROVIDER_NAME);
}
//带参数调用Service的API
public static Service newInstance(String name) throws IllegalAccessException {
Provider p = providers.get(name);
if(p == null) {
throw new IllegalAccessException("没有这个服务的提供者" + name);
}
return p.newService();
}
}
- 在创建参数化类型实例的时候,可以让代码更加简洁
比如:Map中,给我们提供了一个静态工厂
Map<String,List<String>> m = new HashMap<String,List<String>>
//优化
Map<String,List<String>> m = HashMap.newInstance();//一种思路而已,事实上HashMap并没有这个方法
这个思路,可以让我们写在工具类中,或者放在参数化的类中(需要很长的参数化),这样看起来更优雅美观
缺点
- 由于我们要私有化构造方法,因此无法被继承,但是我们可以用装饰器模式等。减少继承
- 与其他静态方法没有区别
常用的命名方式 - valueOf:值是一样的,转换类型
- of:同上
- getInstance:返回的实例是通过方法的参数来确定的,如果是单例则返回同一个实例
- newInstance: 返回不同的实例
- getType:返回工厂方法的类型
- newType:同上
小结
共有构造方法和静态工厂都有自己的优点,静态工厂通常更适合,因此我们在编写工具类的时候或者需要参数化实例化一个类的时候要考虑静态工厂。
1.2遇到多个构造器参数式要考虑用构建器(构造器模式)
多个构造方法,在这种模式下,可以提供第一个只有必要的参数构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推,最后一个构造器包含所有可选参数。这种方式的话,容易会造成错误,如果有相同的变量类型,有时候会犯错~
public class NutritionFacts {
/**必需的**/
private final int servingSize;
/**每一个容器**/
private final int servings;
/**操作**/
private final int calories;
/**g**/
private final int fat;
/**mg**/
private final int sodium;
/**g**/
private final int carbohydrate;
public NutritionFacts(int servingSize,int servings) {
this(servingSize,servings,0);
}
public NutritionFacts(int servingSize,int servings,int calories) {
this(servingSize,servings,calories,0);
}
public NutritionFacts(int servingSize,int servings,int calories,int fat) {
this(servingSize,servings,calories,fat,0);
}
public NutritionFacts(int servingSize,int servings,int calories,int fat,int sodium) {
this(servingSize,servings,calories,fat,sodium,0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
上面的代码可以看到我们自己看都有点累,写起来也要很注意逻辑的调用,客户端调用我们的API的时候也非常难受。
还有一种优化方式,就是用JavaBeans的set方法去优化,setXXXX。这种,但是如果用这种模式的话,难以保证类是不可变的,也就需要想出额外的方法去确保他是线程安全的。对象的一致性也不能得到很好的保证。(是指如果别的地方修改了这个类的一些信息,如果你某一个地方也用了这个类,然后会导致意想不到的错误?)
优雅的方式:使用Builder模式,可以让我们客户端(指别人调用你的API)调用的时候具有很好地可读性。不直接生成对象,而是让客户端直接一些必要的参数,得到一个builder对象,然后就类似setter那样来设置每一个相关的可选参数。最后,客户端调用无参的builder来生成不可变的对象,减少不必要的内存开支。Demo如下:
package secondChapter;
/**
* Builder模式下构建对象,在需要很多构造函数的时候,可以考虑优化成这样
* @author lanyangjia
* @date 2019年4月2日22:17:57
*/
public class NutritionFacts {
/**必需的**/
private final int servingSize;
/**每一个容器**/
private final int servings;
/**操作**/
private final int calories;
/**g**/
private final int fat;
/**mg**/
private final int sodium;
/**g**/
private final int carbohydrate;
//静态成员类
public static class Builder {
//必须的参数
private final int servingSize;
private final int servings;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servings = servings;
this.servingSize = servingSize;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
//返回NutritionFacts对象
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
carbohydrate = builder.carbohydrate;
fat = builder.fat;
sodium = builder.sodium;
}
public static void main(String[] args) {
NutritionFacts nutritionFacts = new NutritionFacts.Builder(240,8)
.calories(100).sodium(35).carbohydrate(27).build();
}
}
builder就像一个构造器一样,会将参数拷贝到真正的对象中,在对象域中对参数进行校验(第39条?);如果违反了某些约束,则抛出IllegalArgumentException。Builder模式的好处在于,可以传递多个可变的参数。如果想创建不同类型的Builder,则可以声明一个接口
/**
* builder泛型接口
* @param <T>
*/
public interface Builder<T> {
public T build();
}
带有Builder实例的方法通常利用有限制的通配符类型来约束构建起的类型参数。例如
Tree builderTree(Builder <? extends Node> nodeBuilder){............}
Builder看上去很不错,但是自己写挺麻烦的,为了创建对象要先创建他的构造器。虽然创建构造器的开销在实践中可能微不足道。但是有时候写的时候代码会很长,因此只有在很多参数的时候才使用。比如说:有4个以上的参数的时候,可以考虑用builder模式。如果一开始就是用构造器或者静态工厂的话,以后我们如果要添加的参数越来越多的时候,构造器的话非常难以维护,看上去也不舒服。
小结
如果类的构造器或者静态工厂中具有逗哥参数,那么设计这种类的时候,Builder模式就是不错的选择,特别是大多数参数是可选的时候。使用Builder模式的客户端代码更易于阅读和编写。
1.3 使用私有构造器或者枚举类型实现单例模式
单例模式,大家都应该不会很陌生,他是只实例化一次的类
饿汉式:
public class Singleton {
//1.将构造方法设为私有,不允许外部直接创建对象
private Singleton(){
}
//2.创建一个实例
private static Singleton instance = new Singleton();
//3提供一个用于获取实例的方法
public static Singleton getInstance(){
return instance;
}
}
懒汉式:
public class Singleton2 {
//将构造方法私有化
private Singleton2(){
}
//声明类的唯一实例
private static Singleton2 instance;
//提供一个对外获取实例的方法
public static Singleton2 getSingleton2() {
if(instance == null)
instance = new Singleton2();
return instance;
}
}
1.4通过私有构造器来强化不可实例化的能力
我们一般自己编写工具类的时候,都会用static来修饰方法,这样用起来方便,但是这样的工具类一般是不用实例化的,所以我们应该把构造方法私有化
public class Test {
private UtilityClass() {
throw new AssertionError();
}
}
这种设计的方法的缺点就是,是的一个类不可以被子类化,所有的构造器必须显示或者隐式地调用超类的构造器,但是在这种模式下,显然是不行的。
1.5 避免创建不必要的对象
一般来说,可以重用对象的尽量重用对象,这样可以减少不必要的开销。
比如说:
String s = new String("test");
"test"本身就是一个对象,如果在循环中或者频繁调用的方法中,这样就会创建成千上万的String实例。
改进后的版本是
String s = "test";
上面说的避免创建不必要的对象,并不是说尽可能地避免创建对象。相反,由于小对象创建和回收动作是非常快的,特别是在JVM优化到现在的层面。通过创建附加的对象,提升程序的功能性,简洁性和功能性,对于开发来说,也是非常有意义的。
正确的来说,应该是可以重用对象的时候,不要创建新的对象。
举例:
public class Person {
private final Date birthDate;
// private static final Date BOOM_START;
// private static final Date BOOM_END;
public Person(Date date) {
this.birthDate = date;
}
public boolean isBabyBoomer() {
Calendar gtmCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gtmCal.set(1946,Calendar.JANUARY,1,0,0,0);
Date boomStart = gtmCal.getTime();
gtmCal.set(1965,Calendar.JANUARY,1,0,0,0);
Date boomEnd = gtmCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthDate.compareTo(boomEnd) < 0;
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
Person person = new Person(new Date());
for(int i=0; i<1000000; i++) {
person.isBabyBoomer();
}
System.out.println("时间花费为" + (System.currentTimeMillis() - start));
}
}
这个程序每次都会创建Calendar,一个TimeZone,两个Date实例.花费的时间为
image.png
改进版:将Calendar,TimeZone,Date实例化一次,而不是每次调用方法的时候创建,优化如下
public class Person {
private final Date birthDate;
private static final Date BOOM_STAR;
private static final Date BOOM_END;
public Person(Date date) {
this.birthDate = date;
}
static {
Calendar gtmCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gtmCal.set(1946,Calendar.JANUARY,1,0,0,0);
BOOM_STAR= gtmCal.getTime();
gtmCal.set(1965,Calendar.JANUARY,1,0,0,0);
BOOM_END = gtmCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_STAR) >= 0 &&
birthDate.compareTo(BOOM_END) < 0;
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
Person person = new Person(new Date());
for(int i=0; i<1000000; i++) {
person.isBabyBoomer();
}
System.out.println("时间花费为" + (System.currentTimeMillis() - start));
}
}
image.png
优化的效率上去了~
tisp:
Java中有自动装箱拆箱的功能。相同条件下Long的花费时间比long更高。因此平时编码要注意哦!
1.6 消除过期对象的引用
观察下面的程序
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[++size] = e;
}
public Object pop() {
if(size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
/**
* 扩容
*/
public void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements,2 * size + 1);
}
}
}
上面的程序我们无论怎样去测试应该都是没有问题的。但是还是会有隐患,随着垃圾回收器活动的增加,或者随着内存占用的不断增加,程序性能会变低。极端情况下会出现OOM
会出现的原因是,一个栈先增长,然后再收缩,那么从栈中弹出的对象将不会当做垃圾回收,即便是这些元素已经不在栈中了。。这是因为,栈内部维护着对这些对象的过期引用(指永远不会解除对这个对象的引用)。
如何解决呢?我们只要告诉JVM,我们不再引用这个对象即可。
public Object pop() {
if(size == 0) {
throw new EmptyStackException();
}
elements[size] = null;
return elements[--size];
}
然而,我们在开发的时候不需要每时每刻都将不用的对象置为null,清空对象引用应该是一种例外,而不是一种规范行为。如果是类管理自己内存的时候,我们就要警惕了。像Stack类,自己会管理内存,存储池中包含了elements数组(对象引用单元,而不是对象本身),垃圾回收器是不知道数组中那些是可用的,哪些是不可用的。因此它误以为里面的所有引用都是有效的,这个时候就需要我们告诉垃圾回收器,那些是不需要的。
(西面两个还没懂,,可能得了解下JVM知识)
- 内存泄露的另外一个常见来源是缓存。
比如说我们放了对象在缓存中,但是我们可能好久没用了,但是缓存中还是会存在的。那么如何解决呢?
缓存外,保存对某个项的键的引用,这个项就有意义,可以用WeakHashMap代表缓存;当缓存中的项过期后,里面的值会自动删除。记住!缓存中是否存在是由该键的外部引用决定的 - 一个是监听器和其他回调
1.7 避免使用终结方法
程序中不要使用finalizer方法。非常危险~
2.对于所有对象都通用的方法
2.1 覆盖equals原则
- 使用 == 操作符检查“参数是否为这个对象的引用”;
- 使用instanceof操作符检查类型是否争取;
- 把参数转为正确的类型
- 对于该类中的关键域,检查参数中的域是否与该对象的中的域相等;
- 覆盖equals方法的时候,总要覆盖hashCode(可以结合HashMap,TreeMap,HashSet等散列表)
- 不要企图让equals方法过于智能;
- 不要将equals中的Objece对象转换为其他类型
2.2覆盖toString方法
建议所有的子类都覆盖toString方法,这对于我们程序的调试或者看一些关键信息很有作用。
2.3 谨慎使用clone方法
这个方法不常用。现在不知道有什么用。。。先不做阐述
2.4考虑实现Comparable接口
一旦类实现了Compareable接口,他就可以跟许多泛型算法以及依赖该接口的集合实现就行协作。(考虑比较排序的时候可以实现这个接口)
3.类和接口
3.1使类和成员的可访问性尽可能低
修饰类,方法,属性等有四种访问级别;private,default,protectd,public。在我们设计类的时候有以下的准则
- 实例域决不能是公有的。如果域是非final的,或者是一个指向可变对象的final引用,那么一旦使这个域变成私有的,那么就放弃了限制能力。。因为,虽然本身是不可以被修改的,但是所引用的对象是可以被修改的。这是线程不安全的,其他也是可以可以进行修改的。因此,还是要把域的可访问性进行限制
- 在仔细设计一个最小的公有API的时候,应该防止把任何散乱的类,接口或者成员变成API的一部分。尽可能地解耦
- 除了公有静态final域的特殊情形外,公有的类都不应该包含公有域,并且要确保公有静态final域所引用的对象都是不可变的。
3.2在公有类中使用访问方法而非公有域
这是一个很常见设计类的时候的规则,就是一般的域都设为私有的,然后对外提供访问和设置的API。但是有时候,需要用包级别或者私有的嵌套类来暴露域,无论这个类是可变的还是不可变的。
3.3使可变性最小化
不可变类只是在其实例化不能被修改的类。每个实例中包含的信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。Java中给我们提供的不可变的类有,String(被final修饰,不可以被子类化),基本类型的包装类,BigInteger和BigDecimal。不可变的类比可变的类更加容易设计,实现,使用。有下面五条规则:
- 不要提供任何会修改对象的状态的方法
- 保证类不会被扩展
- 使所有的域都是final 的
- 使所有的域都是私有的
- 确保对于任何可变组件的互斥访问(也就是说,不提供给客户端任何API获取该实例);并且,永远不要用客户端提供的对象引用来初始化这样的域,因为域指向的引用对象是可变的,有被修改的风险。在构造器,访问方法和readObject方法中使用保护性拷贝
不可变对象的优点和缺点
优点: - 不可变对象本质上是线程安全的,不要求同步
- 可以共享不可变对象,甚至可以共享他们的内部信息
- 为其他对象提供乐然大量的构件
缺点: - 对于每一个不同的值都需要一个单独的对象。
什么时候使用不可变对象?
1.一些频繁被调用的域,计算复杂的可以可以用final修饰(String;延迟初始化)
2.如果不可变的类实现序列化接口,就必须提供一个显示的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectInputStream.readUnshared。
3.不要为每一个get方法编写一个set方法。除非有很好的理由让类变成可变的类,否则就应该是不可变得。
4.构造器应该创建完全初始化的对象(为每一个域都赋值);不要在构造器或者静态工厂之外再提供共有的初始化方法。同时,不要提供重新初始化的方法(这个应该没什么人这么做);
举例:TimerTask类
3.4复合优于继承
我们学Java的时候知道继承是面向对象的三大特征之一,而且也是实现代码复用的重要手段。但它并非永远是完成这项工作的最佳工具。
- 与方法调用不同的是,继承打破了封装性。因为子类依赖其超类中特定功能的实现细节。超类的实现可能随着版本的更替,而不断改变,这个时候由于子类是继承自超类的,这样可能会对子类造成破坏。因此,子类必须跟着其超类的更新而演化,除非超类是专门为了扩展而设计的,并且具有对应的说明文档
- 复用:在新的类中增加一个私有域,它引用现有类的一个实例。这种设计叫做“复合”;
/**
* 实现Set接口
* @author lanyangjia
* @param <E> 参数类型
*/
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
@Override
public int size() {
return s.size();
}
@Override
public boolean isEmpty() {
return s.isEmpty();
}
@Override
public boolean contains(Object o) {
return s.contains(o);
}
@Override
public Iterator<E> iterator() {
return s.iterator();
}
@Override
public Object[] toArray() {
return new Object[0];
}
@Override
public <T> T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean add(E e) {
return s.add(e);
}
@Override
public boolean remove(Object o) {
return s.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}
@Override
public void clear() {
s.clear();
}
}
----------------------------------分割线-----------------------------------------------------------------------
import java.util.Collection;
import java.util.Set;
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
观察以上代码:本质上来讲InstrumentedSet类实现了Set接口,并且拥有单个构造器。这个把一个Set转变成了另外一个Set,同时增加了计数功能。这样的设计带来了很多好处,可以包装任何Set实现,并且可以结合任何先前存在的构造器一起工作,例如:
Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>());
Set<E> s2 = new InstrumentedSet<E>(new HashSet<>());
甚至可以代替原来没有计数功能的Set实例
因为每一个InstrumentedSet都把另外一个Set实例包装起来,所以InstrumentedSet也叫做包装类。包装类几乎没什么缺点,唯一的缺点就是不适用于有回调功能的情况。在回调功能中,对象把自身的引用传递给其他对象,用于后续的调用(“回调”)。因为被包装起来的对象并不知道他外面的包装对象,所以它传递一个指向自身的引用,回调的时候避免了外面的包装对象。
从上面可以看出:
1.只有当子类真正是超类的子类型的时候,才适合用继承。换句话说,对于两个类A,B只有两者之间确实存在is-a关系的时候,B才应该扩展A。每次要用继承的时候要先问问自己,B是不是也是A的一种呢?如果不是,那么一般情况下,B应该包含A的一个私有实例,并且暴露一个较小的,简单的API
2.超类中的API是否有缺陷呢?如果有,就可以利用复合来设计新的API来隐藏这些缺陷。
总结:
- 继承功能非常强大, 但是违背了封装的原则。只有当超类,子类确实存在is-a关系的时候才考虑用继承。
- 如果子类和超类处在不同的包中,并且超类不是为了继承而设计的,那么继承将会导致脆弱性。
- 可以利用复合来代码继承,尤其是在存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能更加强大!
3.5使用继承的时候要注意的问题
- 使用继承的时候要注意写好文档,那些是可覆盖的(文档必须指明什么该方法或者构造器调用了哪些可覆盖的方法,调用的顺序,又会如何影响后面的结果);覆盖的要求是什么
- 决不能在构造器中调用可覆盖的方法
- 如果设计一个类中实现了Serizlizable接口,并且该类有一个readResolve或者writeReplace方法的,就必须使readResolve或者writeReplace成为受保护的方法。
3.6接口优于抽象类
为什么?
- 现有的类容易被更新,以实现新的接口
- 接口是定义mixin(混合类型)的理想选择。例如:Comparable接口,可以允许不同实例进行排序。
- 接口允许我们构造非层次结构的关系。比如时候:水果包含了苹果,这是一种层次结构。而苹果,香蕉他们是同级的没有上下级之分。
骨架
通过结合抽象类和接口的优点,可以设计一个通用的骨架。比如JDK中我们熟悉的AbstractSet,AbstractList。。。。他们成为集合的骨架 。具体的如何实现可以看看JDK的源码。。书里说的demo语言确实有点拗口。
优点:
演变比接口简单,试想一下,如果我们单纯实现了一个接口,然后你新增了一个方法,这样的话,你就不得不去让实现了这个接口的类,再一次覆盖这些方法。如果使用抽象类的话,就可以直接在父类中增加你想增加的方法,这样扩展会更加简单
总结 - 接口的使用比抽象类相对来说更加广泛,但是接口的设计一定要非常谨慎,因为一旦设计成接口了。再想改接口的话工作量会变大
- 如果我们需要演变更加灵活的时候选择抽象类比接口更加合适
- 每次设计完接口或者抽象类要进行全面的测试,确认设计的接口或者实现类没有BUG
3.7不要用接口定义常量
我们知道接口里面也可以定义常量,而且都是static final的,但事实上很少人这么做,也不推荐这么做
3.8使用函数对象表示策略(策略模式)
策略表示你传入不同的参数,或者说你采用不同的策略会返回不同的结果,比如说你传入两个参数,你采用加法策略返回的时候加法的结果,采用除法策略会返回除法的结果。。。。
我们在设计策略类的时候,还需要定义一个策略接口
public interface Comparator<T> {
public int compare(T t1,T t2);
}
class XXX implements Comparator{
////
}
具体的策略类,往往使用匿名类声明,demo如下
String[] stringArray = new String[] {"a","b","c","d"};
//每次使用会创建一个Comparator实例,可以考虑放在final里面
Arrays.sort(stringArray, Comparator.comparingInt(String::length));
总结:
1.当我们实现策略模式的时候,需要声明一个接口来表示该策略;然后为每一个具体策略声明一个实现了该接口的类
2.当一个具体策略只被执行一次的时候,通常使用匿名类来声明和实现这个具体策略类
3.当一个具体策略设计重复使用的时候,可以将类实现为私有的静态成员类;并通过公有的静态final域被导出;如下
public class Host {
private static class StrLenCmp implements Comparator<String>, Serializable {
@Override
public int compare(String t1, String t2) {
return 0;
}
}
public static final Comparator<String> STRING_COMPARATOR = new StrLenCmp();
}
3.9优先使用静态成员类
我们知道内部类主要4种,静态成员类,非静态成员类,匿名类,局部类(很少用)。静态成员类和非静态成员类的区别就是关键字,有没有static,理论上来说:非静态成员类在被创建的时候,它和外围之间的实例之间的关系也就被构建起来了。(这个很少用)。不过JDK中也有用到。比如Map中的keySet,entrySet和Values方法返回。。
如果静态成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,如果省略了static,那么每一个实例创建的时候都会额外包括一个指向外围对象的引用,造成不必要的开销
总结:
1.如果一个嵌套类需要在单个方法之外仍然可见的,或者它太长了,不适合放在方法内部,可以使用成员类
2.如果成员类每一个实例都需要一个指向外围实例的引用,就要把成员类做成非静态的;否则,做成静态的
3.假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,可以把它做成匿名类。
4.泛型
泛型是在编译的时候检查我们的集合类型是否正确,建议在开发的时候就确定好类型,而且不要和数组互相转换。因为数组是运行时类型安全,泛型是编译的时候类型安全,在运行的时候会进行泛型擦除。
关于泛型的建议:
- 优先考虑泛型
- 优先考虑泛型方法
- 优先考虑用泛型方法(在开发中,如果我们能保证返回的类型是正确的,则可以用@SuppressWarnings注解来标识)
- 利用有限制通配符来提升API的灵活性:PECS:producer-extends,consumer-super
5.枚举和注解
5.1 用枚举代替Int常量
int类型如果我们不注意的话非常容易犯错,如以下代码:
public staitc final int APPLE_FUJI = 0;
public static final int ORAGLE_NAVEL = 0;
int i = (APPLE_FUJI - ORAGLE_NAVEL) / ....
如果你将苹果和橙子进行比较,编译器不会任何警告
优化:
public enum Apple{FUJI,PIPPIN}
public enum Orange{NAVEL,TEMPLE}
枚举是一种构造方法私有化,域为final的特殊类。他们是单例的泛型化。如果第一个参数声明为Apple,那么我们后面只能从Apple里面取。
其他更高级的enum用法,尚未接触,故跳过
5.2注解优于命名模式
注解的出现,让命名模式的缺点得以改善(让我们在正确的位置,避免因为起错名而导致程序无法执行)
//运行的时候执行
@Retention(RetentionPolicy.RUNTIME)
//在什么地方上声明,方法,类,域?
@Target(ElementType.METHOD)
public @interface Test {
}
public class Sample {
@Test
public static void m1(){} //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(){}
public static void m6(){}
@Test
public static void m7() { //Test should fail
throw new RuntimeException("Crash");
}
public static void m8(){}
}
上面会有两项失败,一项会通过,另一项无效
如果想指定抛出的异常
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
总结
大多数程序员都不用定义注解类型,但是所有的程序员都应该使用Java平台提供的预定义的注解类型。
6.方法
6.1对参数进行有效的校验
有时候我们写方法的时候,为了避免出现不必要的错误,需要对参数进行校验,让方法很好地运行
6.2谨慎设计方法签名
- 谨慎地选择方法的名称;
- 不要过于追求便利的方法:每一个方法都应该尽其所能。方法太多会使类难以学习和维护。
- 避免过长的参数列表。最好控制在四个以内,如果过多了考虑用对象封装起来。(如果参数过多,而且可选的时候,可以考虑用Builder模式来优化。)
- 对于参数类型,要优先使用接口来接收;比如public void test(Map<String,Objecet>);这一个方法的话可以接收TreeMap,HashMap等子映射表
- 对于要传入boolean类型的,优先使用两个元素的枚举类型。它使代码更易于阅读和编写。比如:
你可能会有有个Thermometer类型,它带有一个静态工厂方法,而这个静态工厂方法的签名需要传入这个枚举的值:
public enum TemperatureScale{FAHRENHEIT,CELSIUS}
Thermometer.newInstacne(TemperatureScale.CELSIUS)比Thermometer.newInstacne(TemperatureScale.true)更有用,而且以后可能会增加新的枚举类型。
6.3 慎用重载
举个例子:
package seven;
import java.math.BigInteger;
import java.util.*;
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> list) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?> []collection = {
new HashSet<String>(),
new ArrayList<BigInteger>()
};
for (Collection<?> objects : collection) {
System.out.println(classify(objects));
}
}
}
image.png
输出的结果都是Unknown Collection.因为重载的话,编译器的选择是静态的。在运行的时候编译器已经选择好了运行哪个方法
如何我想用重载,但是不想出现上面的情况呢?
1.永远不要导出两个具有相同参数的数目的重载方法
2.使用别名,不要使用相同的名字。比如ObjectOutpuStream里面
image.png
- 谨慎使用可变参数(几乎在平时工作中没用到过。。。。)
6.4返回零长度的数组或者集合,而不是null
比如:
private final List<Cheese> cheessList = ....
public Cheese[] getCheese() {
if(cheessList.size == 0) {
return null;
}
}
如果我们把一个返回值设为null,那么客户端需要处理的情况就会很多,需要判断你的返回值是否为空;很容易让人犯错,如果一个没判空就容易造成空指针。。。或者如果前后端交互的话,前端就需要写大量的校验代码。
如何优雅的避免呢?
public class Cheese {
private List<Cheese> cheeseList = ....;
private static final List<Cheese> returnResult = new ArrayList<>();
}
或者
public List<Cheese> getCheeseList() {
if(cheeseList.isEmpty()) {
Collections.emptyList();
}
return new ArrayList<>();
}
Collections.emptySet,emptyList,emptyMap方法提供的正式你所需要的。
总结:
返回类型为数组或者为集合的方法,没理由返回null;正确的做法是返回一个零长度的数组或者集合
6.5 为所有导出的API元素编写文档
我们在开发中其实很少会导出API文档,这些在jdk源码中很重要,或者一些开源的框架很重要。在我们平时的开发中,自己写注释也很重要!!!!!!写注释有时候不是为了方便别人,也是为了方便别人!
image.png
7.通用程序设计
7.1将局部变量作用域最小化
简而言之就是,尽量不要使用全局变量,在方法内部是用局部变量,使变量的作用域最小化
7.2for-each循环优于传统的for循环
for-each是一种很优雅的遍历方式,而且不容易犯错。不过要注意,以下三种情况无法使用for-each:
- 过滤:需要边遍历,边删除一个元素的时候,就需要使用显示的迭代器
- 转换:如果需要在遍历的过程中,取代部分或者全部的元素值,就需要使用列表的迭代器或者数组索引,以便设定元素的值
- 平行迭代:如果需要并行地遍历多个集合,就需要显示地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移
7.3了解和是使用类库
这是一条有意思的建议。比如说我们取随机数的时候,JDK已经为我们提供了随机Random类,我们不需要再绞尽脑汁取想怎么实现,还有集合框架的一些工具类,都是JDK为我们封装好的。总而言之,不要重复造轮子。在做一个需求的时候,先想想有没有相关的工具类,或者已经实现好的类库去供我们使用,这样可以把精力放在程序上,而不是底层的细节上。当然如果你有空的话,可以看看底层一些细节,这样可以看看别人优秀的编码方式,从而从侧面提高自己
7.4如果需要精确答案,请避免使用float和double
float和double都是为了科学计算和工程计算而设计的。它们执行二进制浮点运算。但是它们不适合用于货币计算。正确的做法是使用BigDecimal,int,或者long进行货币计算。BigDecimal有两个缺点,一个是不方便,一个是运行很慢。可以考虑将元转分,用int或者long存储。
如果性能非常关键,并且不介意自己记录十进制的小数点,就可以用int或者long。如果数组没有超过9,就可以使用int,如果不超过18位,则可以使用long。如果数组可能超过18位,则需要用BigDecimal
7.5基本类型优于装箱基本类型
我们知道Java为我们提供了自动的装箱拆箱功能,这非常便利,但是有时候也会为我们带来困扰;比如下面的程序:
public class Test {
static Integer i;
public static void main(String[] args) {
if(i == 42) {
System.out.println("xxxx");
}
}
}
image.png
因为Integer是一个对象,具有额外的非功能值,null。因此会报错
再来一个例子
Comparator<Integer> naturalOrder = new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first < second ? -1 : (first == second ? 0 : 1);
}
};
如果量具有相同的值new Integer(42)和new Integer(42)。这两个相同的,为什么会输出-1呢。因为等号是比较地址的,他们的地址不是一致的,返回false,比较器就会错误地返回1。
下面是正确的姿势,我们只是比较他们的值而已
Comparator<Integer> naturalOrder = new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
int f = first; //Auto-unboxing
int s = second; //Auto-unboxing
return f < s ? -1 : (f == s ? 0 : 1);
}
};
那么什么时候用装箱的基本类型呢?
1.作为集合中的元素,键和值
2.在进行反射的方法调用的时候,必须使用装箱基本类型
总结
1.基本类型要优于装箱基本类型,因为装箱基本类型是对象,会有不必要的开销
2.提防空指针
7.6如果其他类型更适合,避免使用字符串
字符串,感觉我们开发中是非常便捷开发的一种方式,也是下意识的一种开发方式。下面是几条使用字符串的规范
- 字符创不适合带起其他的值类型
- 字符串不适合代替枚举类型(枚举更适合)
- 字符串不适合代替聚集类型
- 字符串也不适合代替能力表。(ThreadLocal)
7.7当心字符串连接的性能
不要使用字符串连接操作合并多个字符串,除非性能无关紧要。相反,应该使用StringBuilder的append方法。另一种方法是,使用字符数组,或者每次只处理一个字符串,而不是将它们组合起来。
7.8通过接口爱引用对象
- 如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,都应该使用接口类进行声明
- 如果程序中使用了只有某个类特有的方法,只能使用类实现
- 如果没有合适的接口存在,完全可以用类而不是接口来引用对象
7.9接口优于反射机制
反射可以让我们通过程序来访问关于已装载的类的信息。通过程序访问类的成员名称、域类型、方法签名等信息。但是反射也要付出代价
- 丧失了编译时类型检查的好处;包括异常检查。如果程序用反射方式来调用不存在的或者不可访问的方法,运行时它将会失败
- 执行反射访问所需要的代码非常笨拙和冗长。
- 性能损失。反射方法调用比普通方法调用慢很多。
因此,普通应用程序在运行的时候不应该以反射方式访问对象
什么时候用反射? - 提供成熟的服务者框架!绝大多数情况下使用反射都是这种情况
7.10谨慎地使用本地方法
我们看源码的时候知道有一些被native修饰的方法,他们称为本地方法,一般是用C or C++编写的。没啥事不要调用就对了!极少数情况下需要使用本地方法来提供性能。(没有熟读底层代码之前还是慎用!)
7.11谨慎地进行优化
- 不要随便优化,不要计较效率上的一些小小的损失
- 要努力编写好的程序而不是快的程序。好的程序体现信息隐藏的原则:只要有可能,他们就会把设计决策集中在单个模块中,因此可以改变单个决策,而不会影响到系统的其他部分。
- 努力避免那些限制性能旧的设计决策
- 每次试图进行优化之前和之后,要对性能进行测量
总结:
不要费力地编写快速的程序,应该努力编写好的程序。在设计系统的时候,特别是设计API的时候,一定要考虑性能因素。如果要优化的话:首先要想到的时候检查所选择的算法!再多的底层优化也无法弥补算法的选择不当
7.12遵守命名规范
- 包名称。通过域名的反写比如com.xxx。鼓励使用有意义的缩写只取首字母缩写也是可以接受的。
- 常量要全部大写而且要以_分割
8.异常
8.1只针对异常情况才使用异常
异常时为了在异常情况下使用而设计的,不要讲它们用于普通的控制流。如果用于普通的控制流,异常的性能开销非常大
8.2对可恢复的情况使用受检异常,对编程错误使用运行时异常
8.3不要过分地使用受检异常
8.4优先使用标准的异常
8.5抛出精确的异常
我们有时候为了方便为catch Exception异常。但是不提倡这么做,最好是catch相对应的异常
8.6异常中需要携带一些有用的信息
我们抛出异常的时候,可以带一些参数,或者有效的信息方便我们排查,不要单纯打印堆栈就算了
8.7努力使失败保持原子性
我们知道原子性是要么都成功,要么都失败。我们在处理异常的时候,如果中间的计算逻辑出现异常了,要保持之前变量的原子性,不能改变其状态。策略有如下策略:
- 调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被改变之前
- 数据库回滚逻辑
- 在操作对象的时候,临时拷贝一份。当所有操作都完成后,采用临时的拷贝对象替换原来的对象。例如Collections.sort在执行排序之前就会先把输入列表转到一个数组里面。如果排序失败,也能保证输入列表的顺序。
9.并发
9.1同步访问共享的可变数据
同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有修改效果(简而言之,就是所有线程看起来数据是一致的。)
- 为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的
- 当多个线程共享可变数据 的时候,每个读或者写都需要同步
9.2不过过度使用同步
同步的性能损耗非常大,虽然说JVM已经做了优化。但是在用同步之前要思考,有没有更好的替代方式~
9.3 executor和task优于线程
我们在使用多线程的时候最优雅的方式是使用线程池,而且最好事自己通过ThreadPoolExecutor类来控制(参考阿里巴巴开发规约)
9.4并发工具优于wait和notify
记得我们刚学习的时候,或者说看马士兵视频的时候都会讲wait,notify。那时候也不知道在生产上有什么用。但是现在几乎没有理由用这两个玩意了。可以使用JUC中提供的并发容器,同步器满足我们的需求(就是信号量,栅栏,倒计数锁,比较常用的是CountDownLatch,Semaphore)
9.5写文档
在创建一个类或者涉及到并发的时候,要用文档进行说明~
9.6慎用延迟初始化
- 在大多数情况下,正常的初始化要优于延迟初始化
- 如果出于性能的考虑而需要对实例域使用延迟初始化,就是用双重检查模式(保证线程安全,单例设计模式在多线程环境下可以这样用)
网友评论