[Java解惑]应用

 

应用... 33
47.      不可变的引用类型... 33
48.      请同时重写equals()hashCode()33
49.      日期设置... 34
50.      IdentityHashMap. 34
51.      静态导入的优先权... 35
52.      PrintStream对输出结果的缓冲... 36
53.      调用操作系统命令时被阻塞问题... 36
54.      实现Serializable的单例问题... 37
55.      thread. isInterrupted()Thread.interrupted()39
56.      惰性初始化... 39
57.      继承内部类... 41
58.      Hash集合序列化问题... 42
59.      迷惑的内部类... 43
60.      编译期常量表达式... 44
61.      打乱数组... 45

应用

47.不可变的引用类型


BigInteger total = BigInteger.ZERO;
total.add(new BigInteger("1"));
total.add(new BigInteger("10"));
System.out.println(total);//0
上面程序的结果为11吗?答案是0
 
BigInteger实例是不可变的。StringBigDecimal以及包装类型:IntegerLongShortByteCharacterBooleanFloatDouble也是如此。对这些类型的操作将返回新的实例。
 
不可变类型更容易设计、实现与作用;它们出错的可能性更小,并且更加安全。
 
本程序修改如下:
BigInteger total = BigInteger.ZERO;
total=total.add(new BigInteger("1"));
total=total.add(new BigInteger("10"));
System.out.println(total);//11

48.请同时重写equals()hashCode()


class T {
       private String str;
 
       T(String str) {
              this.str = str;
       }
 
       public boolean equals(Object obj) {
              if(!(obj instanceof T)){
                     return false;
              }
              T t = (T)obj;
              return t.equals(this.str);
       }
 
       public static void main(String[] args) {
              Set set = new HashSet();
              set.add(new T("str"));
              System.out.println(set.contains(new T("str")));//false
       }
}
上面的程序不会打印true,而是false,为什么?
 
hashCode约定要求相等的对象要具有相同的散列码。
 
无论何时,只要你重写了equals方法,你就必须同时重写hashCode方法。
 
如果将自定的类型对象放入HashSetHashMapHashtableLinkedHashSetLinkedHashMap这此散列集合时,一定需要重写equalshashCode方法,这样在放入进去之后还能查找出来。如果放入其他非散列类型的集合时,其实只需要重写equals就可以了。
 
本程序解决办法重写hashCode()方法:
public int hashCode() {
       return 37 * this.str.hashCode();
}

49.日期设置


Calendar c = Calendar.getInstance();
c.set(2010, 12, 31);// 月是从0开始的,11其实表示12
System.out.println(c.get(Calendar.YEAR) + " " + c.get(Calendar.MONTH));
c = Calendar.getInstance();
c.set(2010, 11, 31);
System.out.println(c.get(Calendar.YEAR) + " " + c.get(Calendar.MONTH));
本程序较简单,只需注意月是从0开始的就可以了,如果你设置月为12,则会自动转换为下一年。

50.IdentityHashMap


class T {
       private String str;
 
       T(String str) {
              this.str = str;
       }
 
       public int hashCode() {
              return 37 * this.str.hashCode();
       }
 
       public boolean equals(Object obj) {
              return this.str.equals(((T) obj).str);
       }
 
       public static void put(Map m) {
              m.put("str", "1");
              /*
               * 由于上面程序将 "str" 放入了字符串常量池,
               * 所以str是同一个对象,不管是什么样类型的
               * Map,即使使用IdentityHashMap都只放入一次
               */
              m.put("str", "2");
              m.put(new T("str"), "3");
              m.put(new T("str"), "4");
       }
 
       public static void main(String[] args) {
              Map m = new HashMap();
              put(m);
              System.out.println(m.size());// 2
              //IdentityHashMap比较时使用==替换equals()方法
              m = new IdentityHashMap();
              put(m);
              System.out.println(m.size());// 3
       }
}

51.静态导入的优先权


import static java.util.Arrays.toString;
import java.util.Arrays;
public class T {
       public static void main(String[] args) {
              prt(1, 2, 3);
       }
       static void prt(Object... args) {
              // 自身继承至Object类的toString的优先级高于静态导入的方法
              //!! System.out.println(toString(args));//不能编译
              System.out.println(Arrays.toString(args));
       }
}
 
本身就属于某个范围的成员在该范围内与静态导入相比具有优先权。

52.PrintStream对输出结果的缓冲


public static void main(String[] args) {
       String str = "Hello World";
       for (int i = 0; i < str.length(); i++) {
              System.out.write(str.charAt(i));
       }
}
上面的程序没有输出结果。
 
这里的问题在于System.out是带有缓冲的。输出的结果被写入了System.out的缓冲区,但是缓冲区从来都没有被刷新。大多数人认为,当有输出产生的时候System.outSystem.err会自动地进制刷新,但这并不完全正确,这两个流都属于PrintStream类型,请看API DOC描述:一个PrintStream被创建为自动刷新,这意味着当一个字节数组(byte[])被写入、或者某个println方法被调用、或者一个换行字符或字节('\n')被写入之后,PrintStream类型的flush方法就会被自动调用。
 
令人奇怪的是,如果这个程序用print(char)去替代write(int),它就会刷新System.out并输出结果,这种行为与print(char)的文档是矛盾的,因为其文档叙述道:“打印一个字符,这个字符将根据平台缺省的字符编码方式翻译成一个或多个字节,并且这些字节将完全按照write(int)方法的方式输出。”,但这里没有换行符却也自动的刷新了。类似的,如果程序改用print(String),它也会对流进行刷新。所以调用print方法也是会自动刷新的。
 
请加入到原博客中:PrintStream也可以对OutputStream进行包装并指定编码方式:PrintStream(OutputStream out, boolean autoFlush, String encoding),但实质上也是调用OutputStreamWriter来实现的。
 
System.erreclipse中输出时是红色的字体。

53.调用操作系统命令时被阻塞问题


public static void main(String[] args) throws IOException,
              InterruptedException {
       String command = "java ProcessTest exc";
       if (args.length != 0) {
              for (int i = 0; i < 200; i++) {
                     System.out.println(command);
                     System.err.println(command);
              }
       } else {
              Process process = Runtime.getRuntime().exec(command);        
              int exitValue = process.waitFor();
              System.out.println("exit value = " + exitValue);
       }
}
执行java ProcessTest发现程序阻塞。
 
Process文档描述:由于某些本地平台只提供有限大小的缓冲,所以如果不能迅速地读取子进程的输出流,就有可能会导致子进程的阻塞,甚至是死锁。这恰好就是这里所发生的事情:没有足够的缓冲空间来保存这些输出结果。为了结子进程(Process线程),父进程(Main线程)必须排空它的输出流(标准流与错误流都需要排空),即要去缓存中读取结果:
static void readResult(final InputStream is) {
       new Thread(new Runnable() {
              public void run() {
                     try {
                            // 排空缓存内容
                            while (is.read() >= 0);
                     } catch (IOException e) {
                            e.printStackTrace();
                     }
              }
       }).start();
}
然后在process.waitFor()之前加上
readResult(process.getErrorStream());
readResult(process.getInputStream());
即可输出exit value = 0
 
另外,只能根据process.waitFor返回的结果来判断操作系统命令执行是否成功(成功:0,失败:1),我们不能根据错误流中是否有内容来判断是否执行成功。

54.实现Serializable的单例问题


class Dog implements Serializable{
       public static final Dog INSTANCE = new Dog();
       private Dog(){}
}
上面能控制只生成一个单实例吗?
 
如果对实现了Serializable的对象进行序列化后,再反序列化,内中会不只一个实例了,因为反序列化时会重新生成一个对象。
 
既然INSTANCE为静态域,那序列化时返回的对象如果也是INSTANCE就可以解决问题了,而打开API我们发现Serializable接口确实有这样两个特殊的方法描述:
l  将对象写入流时需要指定要使用的替代对象的可序列化类,应使用准确的签名来实现此特殊方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
writeReplace 方法将由序列化调用,前提是如果此方法存在,而且它可以通过被序列化对象的类中定义的一个方法访问。因此,该方法可以拥有私有 (private)、受保护的 (protected) 和包私有 (package-private) 访问。子类对此方法的访问遵循 java 访问规则。
l  在从流中读取类的一个实例时需要指定替代的类应使用的准确签名来实现此特殊方法:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
readResolve 方法遵循与 writeReplace 相同的调用规则和访问规则。
 
上述两个方法的只要出现,就会履盖以下两个方法(这两个方法本质的意义就是用来替换序列与反序列的对象),虽然会执行它们,但最后得到的结果却是writeReplacereadResolve两个方法写入或读出的对象:
l  private void writeObject(java.io.ObjectOutputStream out) throws IOException
l  private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException;
 
另外,writeObjectreadObject需成对实现,而writeReplacereadResolve则不需要成对出现,一般单独使用。如果同时出现这四个方法,最后写入与读出的结果以writeReplacereadResolve方法的结果为准。
 
所以下要解决真真单实例问题,我们如下修正:
class Dog implements Serializable {
       public static final Dog INSTANCE = new Dog();
       private Dog() {}
       private Object readResolve() {
              return INSTANCE;
       }
}
 
public class SerialDog {
       public static void main(String[] args) throws IOException,
                     ClassNotFoundException {
              ByteArrayOutputStream bos = new ByteArrayOutputStream();
              new ObjectOutputStream(bos).writeObject(Dog.INSTANCE);
              ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
              Dog dog = (Dog) new ObjectInputStream(bin).readObject();
              System.out.println(dog == Dog.INSTANCE);//true
       }
}
 
一个实现了Serializable的单例类,必须有一个readResolve方法,用以返回它的唯一实例。

55.thread. isInterrupted()Thread.interrupted()


public class SelfInerruption {
       public static void main(String[] args) {
              Thread.currentThread().interrupt();
              if (Thread.interrupted()) {
                     // Interruped:false
                     System.out.println("Interruped:" + Thread.interrupted());
              } else {
                     System.out.println("Not interruped:" + Thread.interrupted());
              }
       }
}
上面结果走的是第一个分支,但结果却不是Interruped:true
 
Thread.interrupted()Thread的静态方法,调用它首先会返回当前线程的中断状态(如果当前线程上调用了interrupt()方法,则返回true,否则为false),然后再清除当前线程的中断状态,即将中断状态设置为false。换句话说,如果连续两次调用该方法,则第二次调用将返回 false
 
isInterrupted()方法为实例方法,测试线程是否已经中断,并不会清除当前线程中断状态。
 
所以这里应该使用isInterrupted()实例方法,就可以修复该问题。

56.惰性初始化


public class Lazy {
       private static boolean initial = false;
       static {
              Thread t = new Thread(new Runnable() {
                     public void run() {
                            System.out.println("befor...");//此句会输出
                            /*
                             * 由于使用Lazy.initial静态成员,又因为Lazy还未
                             * 始化完成,所以该线程会在这里等待主线程初始化完成
                             */
                            initial = true;
                            System.out.println("after...");//此句不会输出
                     }
              });
              t.start();
              try {
                     t.join();// 主线程等待t线程结束
              } catch (InterruptedException e) {
                     e.printStackTrace();
              }
       }
 
       public static void main(String[] args) {
              System.out.println(initial);
       }
}
看看上面变态的程序,一个静态变量的初始化由静态块里的线程来初始化,最后的结果怎样?
 
当一个线程访问一个类的某个成员的时候,它会去检查这个类是否已经被初始化,在这一过程中会有以下四种情况:
1、  这个类尚未被初始化
2、  这个类正在被当前线程初始化:这是对初始化的递归请求,会直接忽略掉(另,请参考《构造器中静态常量的引用问题》一节)
3、  这个类正在被其他线程而不是当前线程初始化:需等待其他线程初始化完成再使用类的Class对象,而不会两个线程都会去初始化一遍(如果这样,那不类会初始化两遍,这显示不合理)
4、  这个类已经被初始化
当主线程调用Lazy.main,它会检查Lazy类是否已经被初始化。此时它并没有被初始化(情况1),所以主线程会记录下当前正在进行的初始化,并开始对这个类进行初始化。这个过程是:主线程会将initial的值设为false,然后在静态块中创建并启动一个初始化initial的线程t,该线程的run方法会将initial设为true,然后主线程会等待t线程执行完毕,此时,问题就来了。
由于t线程将Lazy.initial设为true之前,它也会去检查Lazy类是否已经被初始化。这时,这个类正在被另外一个线程(mian线程)进行初始化(情况3)。在这种情况下,当前线程,也就是t线程,会等待Class对象直到初始化完成,可惜的是,那个正在进行初始化工作的main线程,也正在等待t线程的运行结束。因为这两个线程现在正相互等待,形成了死锁。
 
修正这个程序的方法就是让主线程在等待线程前就完成初始化操作:
public class Lazy {
       private static boolean initial = false;
       static Thread t = new Thread(new Runnable() {
              public void run() {
                     initial = true;
              }
       });
       static {
              t.start();
       }
 
       public static void main(String[] args) {
              // Lazy类初始化完成后再调用join方法
              try {
                     t.join();// 主线程等待t线程结束
              } catch (InterruptedException e) {
                     e.printStackTrace();
              }
              System.out.println(initial);
       }
}
虽然修正了该程序挂起问题,但如果还有另一线程要访问Lazyinitial时,则还是很有可能不等initial最后赋值就被使用了。
 
总之,在类的初始化期间等待某个线程很可能会造成死锁,要让类初始化的动作序列尽可能地简单。

57.继承内部类


一般地,要想实例化一个内部类,如类Inner1,需要提供一个外围类的实例给构造器。一般情况下,它是隐式地传递给内部类的构造器,但是它也是可以以 expression.super(args) 的方式即通过调用超类的构造器显式的传递。
 
public class Outer {
       class Inner1 extends Outer{
              Inner1(){
                     super();
              }
       }
       class Inner2 extends Inner1{
              Inner2(){
                     Outer.this.super();
              }
              Inner2(Outer outer){
                     outer.super();
              }
       }
}
 
class WithInner {
       class Inner {}
}
class InheritInner extends WithInner.Inner {
       // ! InheritInner() {} // 不能编译
       /*
        * 这里的superInheritInner类的父类WithInner.Inner的默认构造函数,而不是
        * WithInner的父类构造函数,这种特殊的语法只在继承一个非静态内部类时才用到,
        * 表示继承非静态内部类时,外围对象一定要存在,并且只能在 第一行调用,而且一
        * 定要调用一下。为什么不能直接使用 super()或不直接写出呢?最主要原因就是每个
        * 非静态的内部类都会与一个外围类实例对应,这个外围类实例是运行时传到内
        * 部类里去的,所以在内部类里可以直接使用那个对象(比如Outer.this),但这里
        * 是在外部内外 ,使用时还是需要存在外围类实例对象,所以这里就显示的通过构造
        * 器传递进来,并且在外围对象上显示的调用一下内部类的构造器,这样就确保了在
        * 继承至一个类部类的情况下 ,外围对象一类会存在的约束。
        */
       InheritInner(WithInner wi) {
              wi.super();
       }
 
       public static void main(String[] args) {
              WithInner wi = new WithInner();
              InheritInner ii = new InheritInner(wi);
       }
}

58.Hash集合序列化问题


class Super implements Serializable{
       // HashSet要放置在父类中会百分百机率出现
       // 放置到子类中就不一定会出现问题了
       final Set set = new HashSet();
}
class Sub extends Super {
       private int id;
       public Sub(int id) {
              this.id = id;
              set.add(this);
       }
       public int hashCode() {
              return id;
       }
       public boolean equals(Object o) {
              return (o instanceof Sub) && (id == ((Sub) o).id);
       }
}
 
public class SerialKiller {
       public static void main(String[] args) throws Exception {
              Sub sb = new Sub(888);
              System.out.println(sb.set.contains(sb));// true
             
              ByteArrayOutputStream bos = new ByteArrayOutputStream();
              new ObjectOutputStream(bos).writeObject(sb);
             
              ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
              sb = (Sub) new ObjectInputStream(bin).readObject();
             
              System.out.println(sb.set.contains(sb));// false
       }
}
 
Hash一类集合都实现了序列化的writeObject()readObject()方法。这里错误原因是由HashSetreadObject方法引起的。在某些情况下,这个方法会间接地调用某个未初始化对象的被覆写的方法。为了组装正在反序列化的HashSetHashSet.readObject调用了HashMap.put方法,而put方法会去调用键的hashCode方法。由于整个对象图正在被反序列化,并没有什么可以保证每个键在它的hashCode方法被调用时已经被完全初始化了,因为HashSet是在父类中定义的,而在序列化HashSet时子类还没有开始初始化(这里应该是序列化)子类,所以这就造成了在父类中调用还没有初始完成(此时id0)的被子类覆写的hashCode方法,导致该对象重新放入hash表格的位置与反序列化前不一样了。hashCode返回了错误的值,相应的键值对条目将会放入错误的单元格中,当id被初始化为888时,一切都太迟了。
 
这个程序的说明,包含了HashMapreadObject方法的序列化系统总体上违背了不能从类的构造器或伪构造器(如序列化的readObject)中调用可覆写方法的规则。
 
如果一个HashSetHashtableHashMap被序列化,那么请确认它们的内容没有直接或间接地引用它们自身,即正在被序列化的对象。
 
另外,在readObjectreadResolve方法中,请避免直接或间接地在正在进行反序列化的对象上调用任何方法,因为正在反序列化的对象处于不稳定状态。

59.迷惑的内部类


public class Twisted {
       private final String name;
       Twisted(String name) {
              this.name = name;
       }
       // 私有的不能被继承,但能被內部类直接访问
       private String name() {
              return name;
       }
       private void reproduce() {
              new Twisted("reproduce") {
                     void printName() {
                            // name()为外部类的,因为没有被继承过来
                            System.out.println(name());// main
                     }
              }.printName();
       }
 
       public static void main(String[] args) {
              new Twisted("main").reproduce();
       }
}
 
在顶层的类型中,即本例中的Twisted类,所有的本地的、内部的、嵌套的长匿名的类都可以毫无限制地访问彼此的成员。
 
另一个原因是私有的不能被继承。

60.编译期常量表达式


第一个PrintWords代表客户端,第二个Words代表一个类库:
class PrintWords {
       public static void main(String[] args) {
              System.out//引用常量变量
                            .println(Words.FIRST + " "
                                          + Words.SECOND + " "
                                          + Words.THIRD);
       }
}
class Words {
       // 常量变量
       public static final String FIRST = "the";
       // 非常量变量
       public static final String SECOND = null;
       // 常量变量
       public static final String THIRD = "set";
}
现在假设你像下面这样改变了那个库类并且重新编译了这个类,但并不重新编译客户端的程序PrintWords
class Words {
       public static final String FIRST = "physics";
       public static final String SECOND = "chemistry";
       public static final String THIRD = "biology";
}
此时,端的程序会打印出什么呢?结果是 the chemistry set,不是the null set,也不是physics chemistry biology,为什么?原因就是 null 不是一个编译期常量表达式,而其他两个都是。
 
对于常量变量(如上面Words类中的FIRSTTHIRD)的引用(如在PrintWords类中对Words.FIRSTWords.THIRD的引用)会在编译期被转换为它们所表示的常量的值(即PrintWords类中的Words.FIRSTWords.THIRD引用会替换成"the""set")。
 
一个常量变量(如上面Words类中的FIRSTTHIRD)的定义是,一个在编译期被常量表达式(即编译期常量表达式)初始化的final的原生类型或String类型的变量。
 
那什么是“编译期常量表达式”?精确定义在[JLS 15.28]中可以找到,这样要说的是null不是一个编译期常量表达式。
 
由于常量变量会编译进客户端,API的设计者在设计一个常量域之前应该仔细考虑一下是否应该定义成常量变量。
 
如果你使用了一个非常量的表达式去初始化一个域,甚至是一个final或,那么这个域就不是一个常量。下面你可以通过将一个常量表达式传给一个方法使用得它变成一个非常量:
class Words {
       // 以下都成非常量变量
       public static final String FIRST = ident("the");
       public static final String SECOND = ident(null);
       public static final String THIRD = ident("set");
       private static String ident(String s) {
              return s;
       }
}
 
总之,常量变量将会被编译进那些引用它们的类中。一个常量变量就是任何常量表达式初始化的原生类型或字符串变量。且null不是一个常量表达式。

61.打乱数组


class Shuffle {
       private static Random rd = new Random();
       public static void shuffle(Object[] a) {
              for (int i = 0; i < a.length; i++) {
                     swap(a, i, rd.nextInt(a.length));
              }
       }
       public static void swap(Object[] a, int i, int j) {
              Object tmp = a[i];
              a[i] = a[j];
              a[j] = tmp;
       }
       public static void main(String[] args) {
              Map map = new TreeMap();
              for (int i = 0; i < 9; i++) {
                     map.put(i, 0);
              }
             
              // 测试数组上的每个位置放置的元素是否等概率
              for (int i = 0; i < 10000; i++) {
                     Integer[] intArr = new Integer[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
                     shuffle(intArr);
                     for (int j = 0; j < 9; j++) {
                            map.put(j,(Integer)map.get(j)+intArr[j]);
                     }
              }
              System.out.println(map);
              for (int i = 0; i < 9; i++) {
                     map.put(i,(Integer) map.get(i)/10000f);
              }
              System.out.println(map);
       }
}
上面的算法不是很等概率的让某个元素打乱到其位置,程序运行了多次,大致的结果为:
{0=36031, 1=38094, 2=39347, 3=40264, 4=41374, 5=41648, 6=41780, 7=41188, 8=40274}
{0=3.6031, 1=3.8094, 2=3.9347, 3=4.0264, 4=4.1374, 5=4.1648, 6=4.178, 7=4.1188, 8=4.0274}
 
如果某个位置上等概率出现这9个值的话,则平均值会趋近于4,但测试的结果表明:开始的时候比较低,然后增长超过了平均值,最后又降下来了。
 
如果改用下面算法:
public static void shuffle(Object[] a) {
       for (int i = 0; i < a.length; i++) {
              swap(a, i, i + rd.nextInt(a.length - i));
       }
}
多次测试的结果大致如下:
{0=40207, 1=40398, 2=40179, 3=39766, 4=39735, 5=39710, 6=40074, 7=39871, 8=40060}
{0=4.0207, 1=4.0398, 2=4.0179, 3=3.9766, 4=3.9735, 5=3.971, 6=4.0074, 7=3.9871, 8=4.006}
所以修改后的算法是合理的。
 
另一种打乱集合的方式是通过Api中的Collections工具类:
public static void shuffle(Object[] a) {
       Collections.shuffle(Arrays.asList(a));
}
其实算法与上面的基本相似,当然我们使用API中提供的会更好,会在效率上获得最大的受益。

posted @ 2015-01-29 00:24  江正军  阅读(689)  评论(0编辑  收藏  举报