本文记录了,学习Effective Java 2版的心得
情况:大二,学习JavaSE半年后,没有设计和项目经验.
代码实例:大部分摘自原书或博客中,也可能少摘选.
大优势(主要缺点):摘自原书中.
其他:首先感谢知了堂-阳哥在学习道路上长达一年的陪伴,也很庆幸在知了堂遇见鑫鑫.
如有侵权或者错误,请务必留言,萌新感谢大佬的指点.
1.考虑用静态工厂方法替代构造器
用FruitFactory工厂类创建Apple或者Grape类,引用了Java中的反射机制(在运行期动态加载类)
1 package staticfactory; 2 3 public class FruitFactory { 4 public static Fruit getFruit(String type){ 5 Fruit f = null; 6 try { 7 // 通过反射机制在运行期创建实例 8 f = (Fruit)Class.forName("staticfactory." + type).newInstance(); 9 } catch(Exception e){ 10 System.out.print("目前无法生产这种水果"); 11 } 12 return f; 13 } 14 15 public static void main(String[] args) { 16 Fruit f = FruitFactory.getFruit("Apple"); 17 f.say(); 18 } 19 }
1 package staticfactory; 2 3 public class Apple implements Fruit { 4 5 @Override 6 public void say() { 7 System.out.println("I am Apple"); 8 } 9 10 } 11 12 13 package staticfactory; 14 15 public class Apple implements Fruit { 16 17 @Override 18 public void say() { 19 System.out.println("I am Apple"); 20 } 21 22 }
- Apple 和 Grape都实现(扩展)了Fruit接口(父类),Java中利用接口实现多继承
- 这样做的原因是:使得FruitFactory返回的类型统一都返回Fruit类(将Apple或Grape类上转型为Fruit父类,不能调用子类新增的属性和方法)
1 package staticfactory; 2 3 public interface Fruit { 4 public void say(); 5 }
大优势:
- 它们有名称
- 不必在每次调用它们的时候都创建一个新对象
- 它们可以返回原返回类型的任何子类型的对象
- 在创建参数化类型实例的时候,它们使代码变得更加简洁
主要缺点:
- 类如果不含公有的或者受保护的构造器,就不能被子类化
- 它们与其他的静态方法实际上没有任何区别
2.遇到多个构造器参数时要考虑用构建器
在NutritionFacts中声明一个静态内部类Builder,利用Builder类的类似setter方法(会返回当前Builder对象)来为Builder类初始化,全部初始化后利用build()方法返回一个Nutrition对象.
Nutrition中有一个私有构造方法,它通过深拷贝Builder的属性来完成初始化.
1 package telescopingConstructor; 2 3 public class NutritionFacts { 4 // required 5 private final int servingSize; 6 private final int servings; 7 8 // optional 9 public final int calories; 10 public final int fat; 11 public final int sodium; 12 public final int carbohydrate; 13 14 public static class Builder { 15 // required 16 private final int servingSize; 17 private final int servings; 18 19 // optional 20 public int calories; 21 public int fat; 22 public int sodium; 23 public int carbohydrate; 24 25 public Builder(int servingSize, int servings) { 26 super(); 27 this.servingSize = servingSize; 28 this.servings = servings; 29 } 30 31 public Builder calories(int val){ 32 this.calories = val; 33 return this; 34 } 35 36 public Builder fat(int val) { 37 this.fat = val; 38 return this; 39 } 40 41 public Builder carbohydrate(int val){ 42 this.carbohydrate = val; 43 return this; 44 } 45 46 public Builder sodium(int val) { 47 this.sodium = val; 48 return this; 49 } 50 51 public NutritionFacts build(){ 52 return new NutritionFacts(this); 53 } 54 } 55 56 private NutritionFacts(Builder builder){ 57 servingSize = builder.servingSize; 58 servings = builder.servings; 59 calories = builder.calories; 60 fat = builder.fat; 61 sodium = builder.sodium; 62 carbohydrate = builder.carbohydrate; 63 } 64 65 @Override 66 public String toString(){ 67 return servingSize + " " + servings + " " + calories + " " + fat + " " + sodium + " " + carbohydrate; 68 } 69 70 public static void main(String[] args) { 71 NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build(); 72 System.out.println(cocaCola.toString()); 73 } 74 75 }
- Builder中setter方法要返回当前Builder对象,方便一系列的setter方法调用
- Builder通过builder方法调用NutritionFacts构造方法(参数为当前Builder对象),来完成真正的对象创建
- 为了方便和占用少量内存,Builder为静态
优势:
- 客户端代码易于编写,易于阅读.bulider模式模拟了具名的可选参数,就像Ada和Python中的一样
- Builder模式十分灵活,可以利用单个builder构建多个对象(因为实际上是调用其他类的构造方法来拷贝信息并返回这个新对象,所以可以声明多个builder方法来构建多个对象).builder的参数可以在创建对象期间进行调整(可以在拷贝期间进行修改),也可以随着不同的对象而改变.builder可以自动填充某些域,例如每次创建对象时自动增加序列号.
- 比JavaBeans更加安全
劣势:
- 为了创建对象,必须先创建它的构造器.虽然构建器的开销在实践中可能不那么明显.在十分注重性能的情况下,可能成问题.
- Builder模式比重叠构造器模式更加冗长,因为在很多参数的时候才使用,比如4个或者更多.
- 与过时的构造器或者静态工厂显得十分不协调,通常最好一开始就使用构建器.
以上2018/10/07 23:05更新
3.用私有构造器或者枚举类型强化Singleton属性
Singleton指仅仅被实例化一次的类.
1 public class Sengleton_Lyze { 2 //1,构造方法私有化 3 private Sengleton_Lyze(){ 4 } 5 6 //2,创建类的位唯一实例,private static 修饰 7 private static Sengleton_Lyze instance; 8 9 //3,提供获取实例的方法,public static 修饰 10 public static Sengleton_Lyze getInstance(){ 11 if (instance==null) { 12 instance=new Sengleton_Lyze(); 13 } 14 return instance; 15 } 16 }
1 /** 2 * 单例模式Sengleton 3 * 试用实际场合:有些对象只需要一个就够了。 4 * 作用:保证整个实际应用程序中某个实例有且只有一个 5 * 类别:饿汉模式,懒汉模式 6 * 区别:饿汉模式的特点,加载类时比较慢,获取比较慢。线程安全的。 7 * 8 * 9 */ 10 public class Sengleton_Hunger { 11 //1,构造方法私有化,不允许外接直接创建对象 12 private Sengleton_Hunger(){ 13 } 14 //2,创建类的唯一实例,使用private static 修饰 15 private static Sengleton_Hunger instance = new Sengleton_Hunger(); 16 17 //3,获取这个实例的方法 18 public static Sengleton_Hunger getInstance(){ 19 return instance; 20 } 21 22 }
- 序列化:为了让成为单例的类实现 序列化的(Serializable),我们仅仅在声明上加上 "implements Serializable" 是不够的。 为了维护并保证 单例,必须要声明所有实例域都是 瞬时(transient) 的,并提供一个readResolve方法。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例。
- 但是如果客户端拥有 AccessibleObject.setAccessible() 的权限,就可以通过反射机制调用私有构造器
- 这里实现单例主要是依靠构造操作只执行一次,构造器返回保存好的对象
1 public class Test { 2 public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { 3 //饿汉模式 4 Constructor sengleton_Hunger = Sengleton_Hunger.class.getDeclaredConstructor(); 5 sengleton_Hunger.setAccessible(true); 6 Sengleton_Hunger s1= (Sengleton_Hunger) sengleton_Hunger.newInstance(); 7 Sengleton_Hunger s2= (Sengleton_Hunger) sengleton_Hunger.newInstance(); 8 if (s1==s2) { 9 System.out.println("true"); 10 }else { 11 System.out.println("false"); 12 } 13 14 //懒汉模式 15 Constructor sengleton_Lyze = Sengleton_Lyze.class.getDeclaredConstructor(); 16 sengleton_Lyze.setAccessible(true); 17 Sengleton_Lyze s3= (Sengleton_Lyze) sengleton_Lyze.newInstance(); 18 Sengleton_Lyze s4= (Sengleton_Lyze) sengleton_Lyze.newInstance(); 19 if (s1==s2) { 20 System.out.println("一样的"); 21 }else { 22 System.out.println("不同的"); 23 } 24 } 25 26 } 27 28 结果 29 false 30 不同的
1 public class Sltn { 2 private static Sltn s = null; 3 private static boolean flag = true; 4 5 private Sltn(){ 6 System.out.println("flag:" + flag); 7 if(flag){ 8 flag = !flag; 9 }else{ 10 try { 11 throw new Exception("duplicate instance create error!" + Sltn.class.getName()); 12 } catch (Exception e) { 13 e.printStackTrace(); 14 } 15 } 16 } 17 18 public static Sltn getInstance(){ 19 if(null == s){ 20 s = new Sltn(); 21 } 22 return s; 23 } 24 }
- 通过反射在一定的条件下,是可以使用 类私有的构造方法来获得不同对象的。
- 通过在类私有构造方法中加入条件,可以防止反射获得第二个实例,但写法有些多余、复杂。
以上摘自:https://www.cnblogs.com/ybbzbb/p/5524261.html
1 package useenum; 2 3 public enum Elvis { 4 INsTANCE; 5 6 public void leaveTheBuilding(){ 7 8 } 9 }
- 简洁,无偿提供序列化机制,绝对防止多次实例化,即使是在面对复炸的序列化或者反射攻击的时候.
- 单元素的枚举类型已经成为实现Singleton的最佳方法
4.通过私有构造器强化不可实例化的能力
在缺少显示构造器的情况下,编译器会自动提供一个共有的,无参的缺省构造器.对于用户而言,这个构造器与其他的构造器没有任务区别.
企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的.该类可以被子类化,并且该子类也可以被实例化.这样做甚至会误导用户,以为这种类是专门为了继承而设计的.
添加一个私有构造器,来阻止在外部被实例化.
1 public class UtilityClass { 2 // Suppress default constructor for noninstantiability 3 private UtilityClass() { 4 throw new AssertionError(); 5 } 6 7 // Remainder omitted 8 }
- AssertionEoor异常???(应该是断言异常不清楚,弄清楚回来填坑)
- 副作用:它使一个类不能被子类化.所有的构造器都必须显式或隐式地调用超类构造器.
以上2018/10/8 23:19更新
5.避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象.重用方式既快速,又流行.
1 import java.sql.Connection; 2 import java.sql.DriverManager; 3 import java.sql.SQLException; 4 5 public class DBUtil { 6 private static final String URL = "jdbc:mysql://127.0.0.1:3306/imooc"; 7 private static final String UNAME = "root"; 8 private static final String PWD = "root"; 9 10 private static Connection conn = null; 11 12 static { 13 try { 14 // 1.加载驱动程序 15 Class.forName("com.mysql.jdbc.Driver"); 16 // 2.获得数据库的连接 17 conn = DriverManager.getConnection(URL, UNAME, PWD); 18 } catch (ClassNotFoundException e) { 19 e.printStackTrace(); 20 } catch (SQLException e) { 21 e.printStackTrace(); 22 } 23 } 24 25 public static Connection getConnection() { 26 return conn; 27 } 28 }
- 静态代码只在类加载时运用一次,关于其他块执行顺序:https://www.cnblogs.com/ibelieve618/p/6403573.html
一种创建多余对象的新方法,称作自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型混用.
1 Integer sum = 0; 2 for(int i=1000; i<5000; i++){ 3 sum+=i; 4 }
-
对象包装类是不变的,即一旦构造了包装类,就不同意更改包装在当中的值。同一时候,对象包装类还是final,因此不能定义它们的子类。
上面的代码sum+=i能够看成sum = sum + i。可是+这个操作符不适用于Integer对象,首先sum进行自己主动拆箱操作。进行数值相加操作,最后发生自己主动装箱操作转换成Integer对象。其内部变化例如以下:
1 sum = sum.intValue() + i; 2 Integer sum = new Integer(result);
- 要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱.
- 小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此.,通过创建附加的对象,提升程序的清晰性,简洁性和功能性,这通常是件好事.
6.消除过期的对象引用
即便有垃圾回收功能也需要考虑内存管理的事情.
1 import java.util.Arrays; 2 import java.util.EmptyStackException; 3 4 public class Stack { 5 6 private Object[] elements; 7 private int size = 0; 8 private static final int DEFAULT_INITIAL_CAPACITY = 16; 9 10 public Stack() { 11 elements = new Object[DEFAULT_INITIAL_CAPACITY]; 12 } 13 14 private void ensureCapacity() { 15 if (elements.length == size) { 16 elements = Arrays.copyOf(elements, 2 * size + 1); 17 } 18 } 19 20 public void push(Object e) { 21 ensureCapacity(); 22 elements[size++] = e; 23 } 24 25 public Object pop() { 26 if (size == 0) { 27 throw new EmptyStackException(); 28 } 29 30 return elements[--size]; 31 } 32 33 }
- 仅有程序员知道size之后的元素不再重要了,但是对于垃圾回收器而言,数组中所有对象引用同等有效,称这类内存泄漏为"无意识的对象保存"更为恰当.
1 public Object pop() { 2 if (size == 0) { 3 throw new EmptyStackException(); 4 } 5 6 Object result = elements[--size]; 7 elements[size] = null; // 清空引用 8 9 return result; 10 }
- elements数组长度没有发生改变,后面保存着null
清空对象引用应该是一种例外,而不是一种规范行为.消除过期引用最好的方法是让包含该引用的变量结束其生命周期.
一般而言,只要类是自己管理内存,程序员就应该警惕内存泄漏问题.一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空.
其他内存泄漏常见来源:缓存,监听器和其他回调.
7.避免使用终结方法
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的.使用终结方法会导致行为不稳定,降低性能,以及可移植性问题.
终结方法的缺点在于不能保证会被及时地执行.从一个对象变得不可能到达开始,到它的终结方法被执行,所花费的这段时间是任意长的.
- 不应该依赖终结方法来更新重要的持久状态.
- 使用终结方法有一个非常严重的性能损失
说真的,我没看懂,剩下的部分
8.覆盖equals时请遵守通用约定
不需要覆盖:
- 类的每个实例本质上都是唯一的
- 不关心类是否提供了"逻辑相等"的测试功能
- 超类已经覆盖了equlas,从超类继承过来的行为对于类也是合适的
应该覆盖:
- 类是私有的或是包级私有的,可以确定它的equlas方法永远不会被调用.可以抛出异常来防止被调用
- 类具有自己特有的"逻辑相等"概念(不同于对象等同的概念)
通用约定:
- 自反性
- 对称性
- 传递性
- 一致性
- 非控性
高质量equals的诀窍:
- 使用==操作符检查"参数是否为这个对象的引用"
- 使用instanceof操作复检查"参数是否为正确的类型"
- 把参数转换成正确的类型
- 对于该类的每个关键域,检查参数中的域是否与该对象中对应的域相匹配
- 当你编写了equals方法之后,应该问自己三个问题;它是否是对称的,传递的,一致的?
- 覆盖equals时总要覆盖hashCode
- 不要企图让equlas过于智能
- 不要将equals声明中的Object对象替换为其他的类型
9.覆盖equals时总要覆盖hashCode
在每个覆盖了equals方法的类中,也必须覆盖hashCode方法.如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap,HashSet和Hanshtable.
下面是约定的内容,摘自Object规范[JavaSE6]
- 在应用程序的执行期间,只要对象equlas方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数.在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致.
- 如果两个对象根据equlas(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法必须产生同样的整数结果.
- 如果两个对象根据equlas(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果.但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列列表的性能.
为不相等的对象产生不相等的散列码.
简单解决方法:
- 把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中
- 对于对象中每个关键域f(指equlas方法中涉及的每个域),完成一下步骤:
a.为该域计算int类型的散列码
i.如果该域是boolean类型,则计算(f ? 1 : 0)
ii:如果该域是byte,char,short或者int类型,则计算(int)f
iii:如果该域是long类型,则计算(int)( f ^ (f >>> 32) )
iv:如果该域是float类型,则计算Float.floatToIntBits(f)
v:如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型计算散列值
vi.如果该域是一个对象引用,并且该类的equlas方法通用递归调用equlas的方法来比较这个域,则同样为这个域递归地调用hashCode.如果需要更复炸的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode.如果这个域的值为null,则返回0(或者其他某个整数,但通常是0).
vii.如果该域是一个数组,则要把每一个元素当做单独的域来处理.也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来.如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法.
b.按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:
result = 31 * result + c;
3.返回result
4.写完hashCode方法之后,问问自己"相等的实例是否具有相等的散列码".要编写单元测试来验证你的推断.如果相等的实例有着不相等的散列码,则要找出原因,并修改错误.
在散列的计算过程中,可以把冗余域排除在外.换句话说,如果一个域的值可以根据参与计算的其他域值计算出来,则可以把这样的域排除在外.
上述步骤1中用到一个非零的初始值,因此步骤2.a中计算的散列值为0的那些初始域,会影响到散列值.如果步骤1中的初始值为0,则整个散列值将不受这些初始域的影响,因为这些初始域会增加冲突的可能性
步骤2.b中的乘法部分使得散列值依赖于域的顺序.如果一个类包含多个相似的域,这样的乘法运算就会产生一个更好的散列函数.例如,如果String散列函数省略了这个乘法部分,那么只是字母顺序不同的所有字符串都会有相同的散列码.
之所以选择31,是因为它是一个奇数数.如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算.使用素数的好处并不明显,但习惯上都是用素数来计算散列的结果.31有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能: 31 - i == ( i << 5 ) - i.现代的VM可以自动完成这种优化.
关于31可以看看这篇文章:https://segmentfault.com/a/1190000010799123
不要试图从散列码计算中排除掉一个对象的关键部分来提高性能.
10.始终要覆盖toString
虽然java.lang.Object提供了toString方法的一个实现,但它返回的字符串通常并不是类的用户所期望看到的.
它包含类的名称,以及一个"@"符号,接着是散列码的无符号十六进制表示法,例如"PhoneNumber@163b91".
toString的通用约定指出,被返回的字符串应该是一个"简洁的,但信息丰富,并且易于阅读的表达形式",
建议所有的子类都覆盖这个方法.
当对象被传递给println,print,字符串联操作符(+)以及assert或者被控制器打印出来时,toString方法会被自动调用.
toString方法应该返回对象中包含的所有值得关注的信息.
在实现toString的时候,必须要做出一个很重要的决定:是否在文档中指定返回值的格式.
指定格式的好处是,它可以被用一种标准的,明确的,适合人阅读的对象表示法.如果你指定了格式,最好再提供一个相匹配的静态工厂或者构造器,以便程序员可以很容易地再对象和它的字符串表示法之间来回转换.
指定格式的不足之处:如果这个类已经被广泛使用,一旦指定格式,就必须始终如一地坚持这种格式.如果不指定格式,就可以保留灵活性,便于在将来的发行版本中增加信息,或则改进格式
以上多摘自书中,少数添加自己理解,第1-10条到此.
我会将这些应用到以后的编程中,后期回来填坑补充自己的理解.