Java超简明入门学习笔记(三),java入门学习笔记

我在编写和维护jOOQ(Java中内部DSL建模的SQL)时遇到过这些。作为一个内部DSL,jOOQ最大限度的挑战了Java的编译器和泛型,把泛型,可变参数和重载结合在一起,Josh
Bloch可能不会推荐的这种太宽泛的API。

Java超简明入门学习笔记(三),java入门学习笔记

Java编程思想第4版学习笔记(三) 第五章
初始化与清理(构造器和垃圾回收机制)        
Java有和C++类似的构造函数来为新创建的对象执行初始化及完成一些特殊的操作,有的类数据成员可能会被初始化几次,它们的初始化次序和次数是根据程序决定的,可以用重载的构造函数以不同的形式对一个对象初始化,重载的构造函数之间可以通过this互相调用。最后,本章讲了finalize()函数和简单的GC机制,也提到了如何创建一个数组。
       知识点1:P76,5.1,定义构造函数      
当对象被创建时,构造函数会自动被调用,构造器的函数名和类名相同,无返回值类型(也不是void类型,就是不需要任何类型),可以有任意个参数,在函数体里写上你想让该类对象被创建时会发生的事情。创建对象时要给对象符合构造器(构造函数的另一种说法)要求的参数。不需要任何参数的构造器被称为“无参构造器”或“默认构造器”。
   
 如果你写的类没有任何构造函数,Java会自动帮你创建出一个无参构造器,它做的事只是把类内成员初始化为0或那个类型默认的初始值。如果你写了一个或多个构造器(无论带不带参数),系统将不再自动生成一个无参构造器。
     一个含有显式构造器的类的实例被初始化的顺序是这样的:    
 在构造函数被调用之前,静态变量首先被初始化为默认值或类内初始值(这一行为被这本书称为指定初始化,具体做法是在类字段定义时就用字段=对象;的方式初始化字段),然后一般变量首先被初始化为默认值或类内初始值。之后,会执行接下来会提到的“初始化子句”,最后,构造函数如果有初始化类数据成员的语句,则,这些语句依次对数据成员初始化。另外,静态类数据成员只要被访问,哪怕没有实例存在,也会完成初始化。
       知识点2:P77,5.2,方法重载      
同一个类里可以有多个同名函数,这些函数的参数类型或参数数量不一样使使用者可以通过不同的参数组合调用一个名字的参数,这种形式叫方法重载。定义一个重载的函数,只需要通过正常的定义函数的形式,把函数名设定为想要重载的函数名,把参数列表变成不一样的就行了。
   
 重载之后,如果传入的实参类型并非任意一个重载函数需要的类型,但经过非窄化转换仍能匹配所有的重载函数需要的参数类型,这个参数就会类型提升为更接近它的那一个函数需要的参数类型,参与运算。
       知识点3:P84,5.4,this      
this可以出现在非静态函数体内,代表当前函数所属对象的一个引用。在一个构造器中调用另一个构造器,必须使用this(构造器参数);的形式,一个构造器中只能通过这种方法调用一次别的构造器,且这个调用要放在函数最开始。
       知识点4:P87,5.5,垃圾回收与finalize()函数
     垃圾回收器要回收对象的时候,首先要调用这个类的finalize方法),一般的纯Java编写的Class不需要重新覆盖这个方法,因为Object已经实现了一个默认的,除非我们要实现特殊的功能。如果想覆盖Object的finalize方法,只需声明一个
protected void finalize( ) { }
这样的函数。在函数里写一些你想要Java的GC回收对象之前你想做的事情。    
 Java中有一个有趣的函数,叫System.gc();,这个函数的作用是提醒JVM:程序员觉得应该进行一次垃圾回收了。至于到底JVM真的开始了垃圾回收,还是没有理你的这个意愿,认为根本没必要浪费时间和资源进行垃圾回收,那就是JVM自己的事情了。
   
 Java垃圾回收的机理在于遍历对象和对象的引用,只要发现某个对象不可达,即在代码执行的某一过程中无法通过任何引用(或者这个对象目前根本就没有引用)访问这个对象,这个对象就失去的存在的意义,会被回收。不过具体的回收时机,不同的JVM实现上也不尽相同。
       知识点5:P87,5.7.3,静态初始化子句和实例初始化子句     
如果你需要在构造函数里额外执行一些语句,又实在懒得写构造函数,你可以使用如下所示的方法(静态/实例初始化子句)实现这个目的。红色的部分就是初始化子句。初始化子句会在默认初始化和构造器初始化之间被调用。
class staticClass{
    static int i = 0;
    static{
        i = 9;
        System.out.println(“i = 9”);
    }
}

class sampleClass{
    int i =0;
    {
        i = 89;
        System.out.println(“i = 89”);
    } }        知识点6:P98,5.8,数组     
数组是一种能够容纳多个元素的重要结构。要定义一个某种类型的数组,只需这样做:类型名[]
数组名; 或者 类型名 数组名[]; ,比如 int[]
a1;  和一些语言不同,不能够用类似C/C++那样的方式指定数组的大小,只能创建一个数组的引用(即数组名)来操控它代表的真正数组对象。使用一个空(值为null)的数组引用也将出现错误,因此要在这个数组引用初始化之后再使用。可以使用初始化表达式,即一对大括号包围起来的一个元素的列表来初始化数组,比如int[]
a = { 1,2,3,4
};,这种初始化方法只能够用于数组定义之处。也可以把另一个数组的引用所指代的对象传递给这个引用,比如int[]
a2 = a;。最后,还可以通过new关键字创建数组对象和其引用,比如int[] c =
new int[30]; 或者 int[] a = new int[]{1,2,3,4}; 来初始化。    
 正如上一章所说,数组可被用于foreach循环。    
 数组对象有一个叫做toString的方法,这个方法不需要参数,可以把数组转换成合适的字符串。也可以使用Array.toString(数组名)的静态方法达到同样的目的。
       知识点7:P102,5.8.1,可变参数列表     
我们有时候可能需要这么一种函数,这个函数需要传入一些参数,这些参数类型是固定的,但我们并不知道会传入几个这个类型的参数。这时我们就需要这个特性。我们可以在参数列表里放一个数组来解决这个问题。
     比如 void func(int 1, char[] c);
我们想要使用这个函数,就可以这么调用它:func(1, new char[]{‘a’,’b’,’c’}); 不过这种写法需要显式创建一个数组对象并且传入,我们可以用这种写法代替原来的函数定义void
func(int i, char… c){};
,这种定义方式可以允许我们这么调用它:func(1,’a’,’b’,’c’);
,大大简略了语法。Java的函数重载时,即使实参可以转化为多种函数需要的形参类型,不过Java有粒度很细的优先转换的规则,可以使这个调用总能匹配到一个“最容易转化”的,因此不会产生二义性调用(C++函数匹配则只有5个等级,每个等级间的各个调用形式是优先度一致的,因此可能产生二义性调用)。
     可变参数列表必须放在参数列表的最后且每个函数只能有一个可变形参。  
     知识点8:P105,5.9,枚举类型     
和其他主流语言一样,Java也提供了枚举类型,它的关键字是enum,用来方便的定义枚举类型,定义枚举类型的方式类似与定义一个类:enum
testSize{ SMALL, MIDIUM, LARGE };
,使用时,可以为testSize枚举类型创建一个实例:testSize s =
testSize.SMALL;
枚举类型的对象有toString,ordinal等方法,ordinal()返回一个从0开始的当前枚举值在枚举类型中被定义时的次序。此外,枚举类型还提供values()静态函数,返回所有枚举情况的集合,使编程者可以遍历每个枚举值。
       第五章 练习题
     练习1、2:创建一个类,它包含一个未初始化的String域,一个在定义时就初始化的String域,一个构造器初始化的String域。验证第一个String域被默认初始化为了null,探究第二和第三个String域的差异。
图片 1

 1 class StringTest{
 2     StringTest(){
 3         s3 = "4567";
 4     }
 5 
 6     String s1;
 7     String s2 = "1234";
 8     String s3;
 9 }
10 
11 public class MainTest {
12     public static void main(String[] args) {
13         StringTest st = new StringTest();
14         System.out.println(st.s1);
15         System.out.println(st.s2);
16         System.out.println(st.s3);
17     }
18 }

练习1、2答案
     练习3、4:创建一个带无参构造器的类,在构造器中打印一条信息,为这个类创建一个对象。在为这个类创建一个重载构造器,这个构造器需要一个字符串做参数,并把接收的字符串也打印出来。
图片 2

 1 class Test{
 2     Test(){
 3         System.out.println("Test");
 4     }
 5 
 6     Test(String s){
 7         System.out.println(s+"test");
 8     }
 9 }
10 
11 public class MainTest {
12     public static void main(String[] args) {
13         Test t = new Test();
14         Test t2 = new Test("1234");
15     }
16 }

练习3、4答案
     练习5、6:创建一个名为Dog的类,它具有重载的bark()方法,此方法根据不同的基本数据类型进行重载,并根据被调用的版本,打印出不同类型的barking,howling等信息。编写对应的主函数调用所有不同版本的方法。再试着写两个构造函数,它们都需要两个不同类型的参数,但是这两个构造函数需要的参数类型顺序正好相反,试试调用它们。
图片 3

 1 class Dog{
 2     Dog(int i){
 3         System.out.println("barking");
 4     }
 5 
 6     Dog(double d){
 7         System.out.println("howling");
 8     }
 9 
10     Dog(char c){
11         System.out.println("Meow~");
12     }
13 
14     Dog(int i, boolean b){
15         System.out.println("I B");
16     }
17 
18     Dog(boolean b, int i){
19         System.out.println("B I");
20     }
21 }
22 
23 public class MainTest {
24     public static void main(String[] args) {
25         Dog d1 = new Dog(1);
26         Dog d2 = new Dog(1.5);
27         Dog d3 = new Dog('c');
28 
29         Dog bi1 = new Dog(1,true);
30         Dog bi2 = new Dog(true,1);
31     }
32 }

练习5、6答案
     练习7:创建一个没有构造器的类,并在main()中创建其对象,用以验证编译器是否真的自动加入了默认构造器。
图片 4

 1 class Test{
 2     int i;
 3     double d;
 4 }
 5 
 6 public class MainTest {
 7     public static void main(String[] args) {
 8         Test t = new Test();
 9         System.out.println(t.i);
10         System.out.println(t.d);
11     }
12 }

练习7答案
     练习8:编写具有两个方法的类,在第一个方法内调用第二个方法两次:第一次调用时不使用this关键字,第二次调用使用关键字,来验证this关键字的作用。
图片 5

 1 class Test{
 2     void method1(){
 3         method2();
 4         this.method2();
 5     }
 6 
 7     void method2(){ System.out.println("Ah,be called!!"); }
 8 }
 9 
10 public class MainTest {
11     public static void main(String[] args) {
12         Test t = new Test();
13         t.method1();
14     }
15 }

练习8答案
     练习9:编写具有两个(重载)构造器的类,并在第一个构造器中通过this调用第二个构造器。
图片 6

 1 class Test{
 2     Test(String s, double i){
 3         this(i);
 4         ss = s;
 5 
 6         System.out.println("name: "+ss);
 7         System.out.println("Area: "+ii);
 8     }
 9 
10     Test(double i){
11         ii = i*i*3.14;
12     }
13 
14     String ss;
15     double ii;
16 }
17 
18 public class MainTest {
19     public static void main(String[] args) {
20         Test t = new Test("Circle",2);
21     }
22 }

练习9答案
     练习10、11:编写具有finalize()方法的类,并在方法中打印消息,在main()中为该类创建一个对象,研究finalize和System.gc()和finalize()的联系。
图片 7

 1 class Test{
 2     protected void finalize(){
 3         //super.finalize();
 4         System.out.println("Ah!!NO!!!");
 5     }
 6 }
 7 
 8 public class MainTest {
 9     public static void main(String[] args) {
10         Test t = new Test();
11         System.gc();
12     }
13 }

练习10、11答案
     练习12:编写名为Tank的类,此类的状态可以是“满的”或者“空的”,其终结条件是:对象是空的,编写finalize()函数以在gc之前检验对象状态。
图片 8

 1 class Tank{
 2     protected void finalize(){
 3         //super.finalize();
 4         if(isFull){
 5             System.out.println("Not Good");
 6         }
 7         else{
 8             System.out.println("Good!");
 9         }
10     }
11 
12     boolean isFull = false;
13 }
14 
15 public class MainTest {
16     public static void main(String[] args) {
17         Tank t = new Tank();
18         System.gc();
19     }
20 }

练习12答案
     练习13:调试书上代码,略。
     练习14:编写一个类,拥有两个静态字符串域,其中一个在定义处初始化,另一个在静态块中初始化。现在加入一个静态方法打印这两个字段的值,查看它们是否都会在被使用之前完成初始化动作。
图片 9

 1 class Test{
 2     static String s1 = "1234";
 3     static String s2;
 4     static{
 5         s2 = "4567";
 6     }
 7 
 8     static void func(){
 9         System.out.println(Test.s1);
10         System.out.println(Test.s2);
11     }
12 }
13 
14 public class MainTest {
15     public static void main(String[] args) {
16         Test.func();
17     }
18 }

练习14答案
     练习15:编写一个含有字符串域的类,并采用实例初始化方式进行初始化。
图片 10

1 class Test{
2     String s1;
3     {
4         s1 = new String("1234");
5     }
6 }

练习15答案
     练习16:创建一个String对象数组,并为每一个元素都赋值一个String,用for循环来打印此数组。
图片 11

1 public class MainTest {
2     public static void main(String[] args) {
3         String[] arrayString = {"1111","2222","3333","4444"};
4         for(String s:arrayString)
5         {
6             System.out.println(s);
7         }
8     }
9 }

练习16答案
     练习17、18:创建一个类,它有一个构造器,这个构造器接收一个String类型的参数。在构造阶段,打印此参数。创建一个该类对象的引用数组,但是不实际地创建对象赋值给该数组。试着运行程序。再试着通过创建对象,再赋值给引用数组,从而完成程序。
图片 12

 1 class Test{
 2     Test(String s){
 3         System.out.println(s);
 4     }
 5 }
 6 
 7 public class MainTest {
 8     public static void main(String[] args) {
 9         Test[] arrayTest = new Test[]{
10                 new Test("123"),
11                 new Test("456")
12         };
13     }
14 }

练习17、18答案
     练习19:创建一个类,它的构造函数接受一个可变参数的String数组。验证你可以向该方法传递一个用逗号分隔的String实参列表,或是一个String[]。
图片 13

 1 class Test{
 2     Test(String... s){
 3     }
 4 }
 5 
 6 public class MainTest {
 7     public static void main(String[] args) {
 8         Test t1 = new Test("111","222");
 9         Test t2 = new Test(new String[]{"333","444"});
10     }
11 }

练习19答案
     练习20:创建一个使用可变参数列表而不是用普通main()语法的主函数main(),打印args数组的传入的命令行参数。
图片 14

1 public class MainTest {
2     public static void main(String... args) {
3         for(String s:args)
4         {
5             System.out.println(s);
6         }
7     }
8 }

练习20答案
     练习21、22:创建一个enum,它包含纸币中最小面值的6种类型,通过values()循环并打印每一个值以及其ordinal()。最后再试着使用带enum类型的switch语句。
图片 15

 1 enum CNY{
 2     CNY1,CNY5,CNY10,CNY20,CNY50,CNY100;
 3 }
 4 
 5 public class MainTest {
 6     public static void main(String[] args) {
 7         for (CNY temp : CNY.values()) {
 8             System.out.println(temp + " " + temp.ordinal());
 9         }
10 
11         CNY cnyTemp = CNY.CNY5;
12         switch (cnyTemp){
13             case CNY1:
14                 System.out.println("1 Yuan");
15                 break;
16 
17             case CNY5:
18                 System.out.println("5 Yuan");
19                 break;
20 
21             case CNY10:
22                 System.out.println("10 Yuan");
23                 break;
24 
25             case CNY20:
26                 System.out.println("20 Yuan");
27                 break;
28 
29             case CNY50:
30                 System.out.println("50 Yuan");
31                 break;
32 
33             case CNY100:
34                 System.out.println("100 Yuan");
35                 break;
36 
37             default:
38                 System.out.println("Error : Fake Money");
39         }
40     }
41 }

练习21、22答案

Java编程思想第4版学习笔记(三) 第五章
初始化与清理(构造器和垃圾回收机制) Java有…

结论

Java是一个野兽。不像其它更理想主义的语言,它慢慢地演进为今天的样子。这可能是一件好事,因为以Java的开发速度就已经有成百上千个警告,而且这些警告只能通过多年的经验去把握。

敬请期待更多关于这个主题的前十名列表!

2. 不要相信你早期的SPI演进判断

向客户提供SPI可以使他们轻松的向你的库/代码中注入自定义行为的方法。当心你的SPI演进判断可能会迷惑你,使你认为你
(不)打算需要附加参数。
当然,不应当过早增加功能。但一旦你发布了你的SPI,一旦你决定遵循语义版本控制,当你意识到在某种情况下你可能需要另外一个参数时,你会真的后悔在SPI中增加一个愚蠢的单参数的方法:

interface EventListener {
    // Bad
    void message(String message);
}

如果你也需要消息ID和消息源,怎么办?API演进将会阻止你向上面的类型添加参数。当然,有了Java8,你可以添加一个defender方法,“防御”你早期糟糕的设计决策:

interface EventListener {
    // Bad
    default void message(String message) {
        message(message, null, null);
    }
    // Better?
    void message(
        String message,
        Integer id,
        MessageSource source
    );
}

注意,不幸的是,defender方法不能使用final修饰符。

但是比起使用许多方法污染你的SPI,使用上下文对象(或者参数对象)会好很多。

interface MessageContext {
    String message();
    Integer id();
    MessageSource source();
}

interface EventListener {
    // Awesome!
    void message(MessageContext context);
}

比起EventListner SPI你可以更容易演进MessageContext
API,因为很少用户会实现它。

规则:
无论何时指定SPI时,考虑使用上下文/参数对象,而不是写带有固定参数的方法。

备注:
通过专用的MessageResult类型交换结果也是一个好主意,该类型可以使用建设者API构造它。这样将大大增加SPI进化的灵活性。

这是一个比Josh Bloch的Effective
Java规则更精妙的10条Java编码实践的列表。和Josh
Bloch的列表容易学习并且关注日常情况相比,这个列表将包含涉及API/SPI设计中不常见的情况,可能有很大影响。

10. 避免方法(T…)签名

在特殊场合下使用“accept-all”变量参数方法接收一个Object…参数就没有错的:

void acceptAll(Object... all);

编写这样的方法为Java生态系统带来一点儿JavaScript的感觉。当然你可能想要根据真实的情形限制实际的类型,比如String…。因为你不想要限制太多,你可能会认为用泛型T取代Object是一个好想法:

void acceptAll(T... all);

但是不是。T总是会被推断为Object。实际上你可能仅仅认为上述方法中不能使用泛型。更重要的是你可能认为你可以重载上述方法,但是你不能:

void acceptAll(T... all);
void acceptAll(String message, T... all);

这看起来好像你可以可选地传递一个String消息到方法。但是这个调用会发生什么呢?

acceptAll("Message", 123, "abc");

编译器将T推断为<? extends Serializable &
Comparable<?>>,这将会使调用不明确!

所以无论何时你有一个“accept-all”签名(即使是泛型),你将永远不能类型安全地重载它。API使用者可能仅仅在走运的时候才会让编译器“偶然地”选择“正确的”方法。但是也可能使用accept-all方法或者无法调用任何方法。

规则: 如果可能,避免“accept-all”签名。如果不能,不要重载这样的方法。

8. 短路式 equals()

这是一个比较容易操作的方法。在比较复杂的对象系统中,你可以获得显著的性能提升,只要你在所有对象的equals()方法中首先进行相等判断:

@Override
public boolean equals(Object other) {
  if (this == other) return true;
  // 其它相等判断逻辑...
}

注意,其它短路式检查可能涉及到null值检查,所以也应当加进去:

@Override
public boolean equals(Object other) {
  if (this == other) return true;
  if (other == null) return false;
  // Rest of equality logic...
}

规则: 在你所有的equals()方法中使用短路来提升性能。

让我与你分享10个微妙的Java编码最佳实践:

1. 牢记C++的析构函数

记得C++的析构函数?不记得了?那么你真的很幸运,因为你不必去调试那些由于对象删除后分配的内存没有被释放而导致内存泄露的代码。感谢Sun/Oracle实现的垃圾回收机制吧!

尽管如此,析构函数仍提供了一个有趣的特征。它理解逆分配顺序释放内存。记住在Java中也是这样的,当你操作类析构函数语法:

  • 使用JUnit的@Before和@After注释
  • 分配,释放JDBC资源
  • 调用super方法

还有其他各种用例。这里有一个具体的例子,说明如何实现一些事件侦听器的SPI:

@Override
public void beforeEvent(EventContext e) {
    super.beforeEvent(e);
    // Super code before my code
}

@Override
public void afterEvent(EventContext e) {
    // Super code after my code
    super.afterEvent(e);
}

臭名昭著的哲学家就餐问题是另一个说明它为什么重要的好例子。
关于哲学家用餐的问题,请查看链接:

*规则:无论何时使用before/after, allocate/free,
take/return语义实现逻辑时,考虑是否逆序执行after/free/return操作。*

4. 现在就开始编写SAM!

Java8的脚步近了。伴随着Java8带来了lambda表达式,无论你是否喜欢。尽管你的API用户可能会喜欢,但是你最好确保他们可以尽可能经常的使用。因此除非你的API接收简单的“标量”类型,比如int、long、String
、Date,否则让你的API尽可能经常的接收SAM。

什么是SAM?SAM是单一抽象方法[类型]。也称为函数接口,不久会被注释为@FunctionalInterface。这与规则2很配,EventListener实际上就是一个SAM。最好的SAM只有一个参数,因为这将会进一步简化lambda表达式的编写。设想编写

listeners.add(c -> System.out.println(c.message()));

来替代

listeners.add(new EventListener() {
  @Override
  public void message(MessageContext c) {
    System.out.println(c.message()));
  }
});

设想以JOOX的方式来处理XML。JOOX就包含很多的SAM:

$(document)
  // Find elements with an ID
  .find(c -> $(c).id() != null)
  // Find their child elements
  .children(c -> $(c).tag().equals("order"))
  // Print all matches
  .each(c -> System.out.println($(c)))

规则:对你的API用户好一点儿,从现在开始编写SAM/函数接口。

备注:有许多关于Java8 lambda表达式和改善的Collections
API的有趣的博客:

6.设计API时永远不要返回空(null)数组或List

尽管在一些情况下方法返回值为null是可以的,但是绝不要返回空数组或空集合!请看
java.io.File.list()方法,它是这样设计的:

此方法会返回一个指定目录下所有文件或目录的字符串数组。如果目录为空(empty)那么返回的数组也为空(empty)。如果指定的路径不存在或发生I/O错误,则返回null。

因此,这个方法通常要这样使用:

File directory = // ...

if (directory.isDirectory()) {
  String[] list = directory.list();

  if (list != null) {
    for (String file : list) {
      // ...
    }
  }
}

大家觉得null检查有必要吗?大多数I/O操作会产生IOExceptions,但这个方法却只返回了null。Null是无法存放I/O错误信息的。因此这样的设计,有以下3方面的不足:
  • Null无助于发现错误
  • Null无法表明I/O错误是由File实例所对应的路径不正确引起的
  • 每个人都可能会忘记判断null情况

以集合的思维来看待问题的话,那么空的(empty)的数组或集合就是对“不存在”的最佳实现。返回空(null)数组或集合几乎是无任何实际意义的,除非用于延迟初始化。

规则:返回的数组或集合不应为null。

3. 避免返回匿名,本地或者内部类

Swing程序员通常只要按几下快捷键即可生成成百上千的匿名类。在多数情况下,只要遵循接口、不违反SPI子类型的生命周期(SPI
subtype lifecycle),这样做也无妨。
但是不要因为一个简单的原因——它们会保存对外部类的引用,就频繁的使用匿名、局部或者内部类。因为无论它们走到哪,外部类就得跟到哪。例如,在局部类的域外操作不当的话,那么整个对象图就会发生微妙的变化从而可能引起内存泄露。

规则:在编写匿名、局部或内部类前请三思能否将它转化为静态的或普通的顶级类,从而避免方法将它们的对象返回到更外层的域中。

注意:使用双层花括号来初始化简单对象:

new HashMap<String, String>() {{
  put("1", "a");
  put("2", "b");
}}

这个方法利用了 JLS
§8.6规范里描述的实例初始化方法(initializer)。表面上看起来不错,但实际上不提倡这种做法。因为要是使用完全独立的HashMap对象,那么实例就不会一直保存着外部对象的引用。此外,这也会让类加载器管理更多的类。

7. 避免状态,使用函数

HTTP的好处是无状态。所有相关的状态在每次请求和响应中转移。这是REST命名的本质:含状态传输(Representational
state
transfer)。在Java中这样做也很赞。当方法接收状态参数对象的时候从规则2的角度想想这件事。如果状态通过这种对象传输,而不是从外边操作状态,那么事情将会更简单。以JDBC为例。下述例子从一个存储的程序中读取一个光标。

CallableStatement s =
  connection.prepareCall("{ ? = ... }");

// Verbose manipulation of statement state:
s.registerOutParameter(1, cursor);
s.setString(2, "abc");
s.execute();
ResultSet rs = s.getObject(1);

// Verbose manipulation of result set state:
rs.next();
rs.next();

这使得JDBC
API如此的古怪。每个对象都是有状态的,难以操作。具体的说,有两个主要的问题:

  • 在多线程环境很难正确的处理有状态的API
  • 很难让有状态的资源全局可用,因为状态没有被描述

规则:更多的以函数风格实现。通过方法参数转移状态。极少操作对象状态。

9. 尽量使方法默认为final

有些人可能不同意这一条,因为使方法默认为final与Java开发者的习惯相违背。但是如果你对代码有完全的掌控,那么使方法默认为final是肯定没错的:

  • 如果你确实需要覆盖(override)一个方法(你真的需要?),你仍然可以移除final关键字
  • 你将永远不会意外地覆盖(override)任何方法

这特别适用于静态方法,在这种情况下“覆盖”(实际上是遮蔽)几乎不起作用。我最近在Apache
Tika中遇到了一个很糟糕的遮蔽静态方法的例子。看一下:

  • TaggedInputStream.get(InputStream))
  • TikaInputStream.get(InputStream))

TikaInputStream扩展了TaggedInputStream,以一种相对不同的实现遮蔽了它的静态get()方法。

与常规方法不同,静态方法不能互相覆盖,因为调用的地方在编译时就绑定了静态方法调用。如果你不走运,你可能会意外获得错误的方法。

规则:如果你完全掌控你的API,那么使尽可能多的方法默认为final。

5.避免让方法返回null

我曾写过1、2篇关于java
NULLs的文章,也讲解过Java8中引入新的Optional类。从学术或实用的角度来看,这些话题还是比较有趣的。

尽管现阶段Null和NullPointerException依然是Java的硬伤,但是你仍可以设计出不会出现任何问题的API。在设计API时,应当尽可能的避免让方法返回null,因为你的用户可能会链式调用方法:

initialise(someArgument).calculate(data).dispatch();

从上面代码中可看出,任何一个方法都不应返回null。实际上,在通常情况下使用null会被认为相当的异类。像
jQuery或
jOOX这样的库在可迭代的对象上已完全的摒弃了null。

Null通常用在延迟初始化中。在许多情况下,在不严重影响性能的条件下,延迟初始化也应该被避免。实际上,如果涉及的数据结构过于庞大,那么就要慎用延迟初始化。

规则:无论何时方法都应避免返回null。null仅用来表示“未初始化”或“不存在”的语义。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图