easy-algorithm-interview-an.../code-languages/java/java 常用习惯用法总结.md

15 KiB
Raw Blame History

注:本文是根据网络上的文章受启发,自己再写代码总结而成。因为无法找到原文的原始出处,所以没法给出原文链接。文章也会引用部分网络素材内容,如果原作者看到请与我联系。

0.前言

本博主的java水平与专业java开发同学相比水平不可同日而语所以平时有空的话会多写写java代码。正好在网上看到一篇关于java习惯用法的总结觉得还不错于是按照文章里的大致架构自己重新实现了一把里面的相关代码相当于自己做个小小的总结。

1.基本方法实现

equals()
hashcode()
compareTo()
clone()

1.1 equals()实例与hashCode()实例

import java.util.Arrays;

public class Person {
    String name;
    int birthday;
    byte[] raw;
    
    public boolean equals(Object obj) {
        // 原文中的代码有误,需要将!符号后面的表达式扩起来
        if (!(obj instanceof Person)) { 
            return false;
        }
        Person other = (Person) obj;
        return name.equals(other.name)
            && birthday == other.birthday
            && Arrays.equals(raw, other.raw);
    }
    
    public int hashCode() {
        return name.hashCode() + birthday + Arrays.hashCode(raw);
    }
    
    public static void main(String[] args) {
        Person leilei = new Person();
        leilei.name = "leilei";
        leilei.birthday = 19991010;
        
        Person lulu = new Person();
        lulu.name = "leilei";
        lulu.birthday = 19991010;
        
        System.out.println(leilei.equals(lulu));
    }

}

equals()方法需要注意的几个点
1.参数必须是Object类型不能是外围类。jdk源码中的equals()方法传入的参数就是Object类型。
2.foo.equals(null) 必须返回false不能抛NullPointerException。(注意null instanceof 任意类 总是返回false因此上面的代码可以运行)
3.基本类型比较使用 ==基本类型数组域的比较使用Arrays.equals()。
4.覆盖equals()时,记得要相应地覆盖 hashCode(),与 equals() 保持一致

hashCode()方法注意的几个点
1.当x和y两个对象具有x.equals(y) == true 你必须要确保x.hashCode() == y.hashCode()。
2.根据逆反命题如果x.hashCode() != y.hashCode()那么x.equals(y) == false 必定成立。
3.你不需要保证当x.equals(y) == false时x.hashCode() != y.hashCode()。但是,如果你可以尽可能地使它成立的话,这会提高哈希表的性能。
4.hashCode()最简单的合法实现就是简单地return 0虽然这个实现是正确的但是这会导致HashMap这些数据结构运行得很慢。

1.2 compareTo()方法实现

package leilei.bit.edu.common;

import java.util.TreeSet;

public class PersonA implements Comparable<PersonA>{
    String firstName;
    String lastName;
    int birthday;
    
    public int compareTo(PersonA other) {
        if(firstName.compareTo(other.firstName) != 0) {
            return firstName.compareTo(other.firstName);
        } else if(lastName.compareTo(other.lastName) != 0) {
            return lastName.compareTo(other.lastName);
        } else if(birthday < other.birthday) {
            return -1;
        } else if(birthday > other.birthday) {
            return 1;
        } else {
            return 0;
        }
    }
    
    @Override
    public String toString() {
        return "firstName: " + firstName + ",lastName: " + lastName;
    }
    
    public static void main(String[] args) {
        PersonA p1 = new PersonA();
        p1.firstName = "li";
        p1.lastName = "lei";
        
        PersonA p2 = new PersonA();
        p2.firstName = "han";
        p2.lastName = "meimei";
        
        PersonA p3 = new PersonA();
        p3.firstName = "xiao";
        p3.lastName = "ming";
        
        TreeSet tree = new TreeSet();
        tree.add(p1);
        tree.add(p2);
        tree.add(p3);
        
        for(Object obj:tree) {
            System.out.println(obj);
        }
    }
}

输出:

firstName: han,lastName: meimei
firstName: li,lastName: lei
firstName: xiao,lastName: ming

TreeSet在java内部是一种排序的数据结构。根据程序的输出可以看出person对象在tree中是根据firstName排序的。

1.总是实现泛型版本 Comparable 而不是实现原始类型 Comparable 。因为这样可以节省代码量和减少不必要的麻烦。
2.只关心返回结果的正负号(负/零/正),它们的大小不重要。
3.Comparator.compare()的实现与这个类似。

1.3 clone()方法

实际中我很少使用clone方法怕自己的理解有偏差直接将原文的clone部分摘过来

class Values implements Cloneable {
  String abc;
  double foo;
  int[] bars;
  Date hired;
 
  public Values clone() {
    try {
      Values result = (Values)super.clone();
      result.bars = result.bars.clone();
      result.hired = result.hired.clone();
      return result;
    } catch (CloneNotSupportedException e) {  // Impossible
      throw new AssertionError(e);
    }
  }
}

1.使用 super.clone() 让Object类负责创建新的对象。
2.基本类型域都已经被正确地复制了。同样我们不需要去克隆String和BigInteger等不可变类型。
3.手动对所有的非基本类型域对象和数组进行深度复制deep copy
4.实现了Cloneable的类clone()方法永远不要抛CloneNotSupportedException。因此需要捕获这个异常并忽略它或者使用不受检异常unchecked exception包装它。
5.不使用Object.clone()方法而是手动地实现clone()方法是可以的也是合法的。

2.应用类

2.1 StringBuilder 与 StringBuffer

这两个类在实际开发中用得很多,举一个很简单的小例子。

    @Test
    public void Str_Build() {
        StringBuilder sb = new StringBuilder();
        sb.append("I ");
        sb.append("love");
        sb.append(" coding");
        System.out.println(sb.toString());
    }

让代码run起来

I love coding

注意点:
1.不要像这样使用重复的字符串连接s += item 因为它的时间效率是O(n^2)。
2.使用StringBuilder或者StringBuffer时可以使用append()方法添加文本和使用toString()方法去获取连接起来的整个文本。示例代码中就是这么做的。
3.优先使用StringBuilder因为它更快。StringBuffer的所有方法都是同步的而你通常不需要同步的方法。
4.StringBuilder的缺陷之一就是开发过程中经常用sb作为new出来的对象名称。。。

2.2 生成一个随机整数

    @Test
    public void int_random() {
        Random rand = new Random();
        int a = rand.nextInt(10) + 1;
        System.out.println("random num is: " + a);
    }

让代码run起来

random num is: 10

2.3 使用Iterator.remove()

    @Test
    public void collections_remove() {
        List<Integer> list = new ArrayList<Integer>();
        for(int i=1; i<=10; i++) {
            list.add(i);
        }
        
        Iterator<Integer> it = list.iterator();
        while(it.hasNext()) {
            int num = it.next();
            
            if(num%2 == 0) {
                System.out.println(num + "===" + num);
                it.remove();
            }
        }
        
        for(int each:list) {
            System.out.print(each + " ");
        }
    }

让代码run起来

2===2
4===4
6===6
8===8
10===10
1 3 5 7 9 

remove()方法作用在next()方法最近返回的条目上。每个条目只能使用一次remove()方法。

2.4 反转字符串

c++的程序猿里的一道经典面试题就是反转字符串。对于java或者python等更高级的语言来说反转字符串都是一句话搞定的事情。

    @Test
    public void reverse_str() {
        String raw_str = "some";
        String rev_str = new StringBuilder(raw_str).reverse().toString();
        System.out.println("rev_str is: " + rev_str);
    }

让代码run起来

rev_str is: emos

2.5 Thread/Runnable

//实现Runnable的方式
void startAThread0() {
  new Thread(new MyRunnable()).start();
}
 
class MyRunnable implements Runnable {
  public void run() {
    ...
  }
}

//继承Thread的方式
void startAThread1() {
  new MyThread().start();
}
 
class MyThread extends Thread {
  public void run() {
    ...
  }
}

匿名继承Thread的方式
void startAThread2() {
  new Thread() {
    public void run() {
      ...
    }
  }.start();
}

不要直接调用run()方法。总是调用Thread.start()方法这个方法会创建一条新的线程并使新建的线程调用run()。

2.6 try-finally

IO流的例子
void writeStuff() throws IOException {
  OutputStream out = new FileOutputStream(...);
  try {
    out.write(...);
  } finally {
    out.close();
  }
}

锁的例子
void doWithLock(Lock lock) {
  lock.acquire();
  try {
    ...
  } finally {
    lock.release();
  }
}

1.如果try之前的语句运行失败并且抛出异常那么finally语句块就不会执行。但无论怎样在这个例子里不用担心资源的释放。
2.如果try语句块里面的语句抛出异常那么程序的运行就会跳到finally语句块里执行尽可能多的语句然后跳出这个方法除非这个方法还有另一个外围的finally语句块

3.输入输出

3.1 从输入流中读取字节数据

    @Test
    public void test1() throws Exception {
        InputStream in = new FileInputStream(new File("xxx"));
        try {
            while(true) {
                int n = in.read();
                if(n == -1) {
                    break;
                } else {
                    System.out.println(n);
                }
            }
            
        } finally {
            in.close();
        }
    }

输出:

112
97
99
107
97
103
101
...

由此可见read()方法要么返回下一次从流里读取的字节数0到255包括0和255要么在达到流的末端。

3.2 从输入流中读取块数据

    @Test
    public void test2() throws Exception {
        InputStream in = new FileInputStream(new File("xxx"));
        try {
            byte[] buf = new byte[100];
            while(true) {
                int n = in.read(buf);
                if(n == -1) {
                    break;
                } else {
                    System.out.println(n);
                }
            }
            
        } finally {
            in.close();
        }
    }

输出:

100
100
100
100
100
100
81

要记住的是read()方法不一定会填满整个buf所以你必须在处理逻辑中考虑返回的长度。

3.3 读取文本文件

    @Test
    public void test3() throws Exception {
        String filename = "xxx";
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(filename)),"utf-8"));
        try {
            while (true) {
                String line = br.readLine();
                if (line == null) {
                    break;
                } else {
                    System.out.println(line);
                }
            }
        } finally {
            br.close();
        }
    }

1.BufferedReader对象的创建显得很冗长。这是因为Java把字节和字符当成两个不同的概念来看待
2.你可以使用任何类型的InputStream来代替FileInputStream比如socket
3.当达到流的末端时BufferedReader.readLine()会返回null。
4.要一次读取一个字符使用Reader.read()方法。
5.你可以使用其他的字符编码而不使用UTF-8但最好不要这样做。

3.4.向文本写文件

    @Test
    public void test4() throws Exception {
        String filename = "xxx";
        PrintWriter out = new PrintWriter(
                new OutputStreamWriter(new FileOutputStream(new File(filename)),"utf-8"));
        try {
            out.println("hello");
            out.println(42);
            out.println("world");
        } finally {
            out.close();
        }
    }

1.Printwriter对象的创建显得很冗长。这是因为Java把字节和字符当成两个不同的概念来看待
2.就像System.out你可以使用print()和println()打印多种类型的值。
3.你可以使用其他的字符编码而不使用UTF-8但最好不要这样做。

4 预防性(Defensive Checking)检测

4.1 预防性检测数值

package leilei.bit.edu.common;

public class Checking {
    
    public static int factorial(int n) {
        if (n < 0) {
            throw new IllegalArgumentException("Undefined");
        } else if(n > 13) {
            throw new ArithmeticException("Reuslt overflow");
        } else if(n == 0) {
            return 1;
        } else {
            return factorial(n-1) * n;
        }
    }
    
    public static void main(String[] args) {
        int n = 5;
        int ret = factorial(n);
        System.out.println("ret is: " + ret);
    }
}

1.不要认为输入的数值都是正数、足够小的数等等。要显式地检测这些条件。
2.一个设计良好的函数应该对所有可能性的输入值都能够正确地执行。要确保所有的情况都考虑到了并且不会产生错误的输出(比如溢出)。

4.2 预防性检测对象

public int findIndex(List<String> list, String target) {
  if (list == null || target == null)
    throw new NullPointerException();
  ...
}

1.不要认为对象参数不会为空null。要显式地检测这个条件。

4.3预防性检测数组索引

    public static void outOfBound(int[] b, int index) {
        if(b==null) {
            throw new NullPointerException();
        }
        if(index < 0 || index >= b.length) {
            throw new IndexOutOfBoundsException();
        }
        ...
    }

不要认为所以给的数组索引不会越界。要显式地检测它。

4.4 预防性检测数组区间

    public static void errorRange(int[] b, int off, int len) {
        if(b == null) {
            throw new NullPointerException();
        }
        if (off < 0 || off > b.length || len < 0 || b.length - off < len) {
            throw new IndexOutOfBoundsException();
        }
        ...
    }

不要认为所给的数组区间比如从off开始读取len个元素是不会越界。要显式地检测它。

5.数组

5.1 填充数组元素

5.2 复制一个范围内的数组元素

5.3 调整数组大小

具体代码如下:

package leilei.bit.edu.bigNum;

import java.util.Arrays;

public class Array_Test {
    
    public static void printArray(int[] a) {
        for(int i=0; i<a.length; i++) {
            System.out.print(a[i] + " ");
        }
    }
    
    public static void fill_test() {
        int[] a = new int[5];
        Arrays.fill(a,0,5,1);
        printArray(a);
    }
    
    public static void copy_test() {
        int[] a = {1,2,3,4,5};
        int[] b = new int[5];
        System.arraycopy(a, 0, b, 0, a.length);
        printArray(b);
    }
    
    public static void copyOf_test() {
        int[] a = {1,2,3,4,5};
        a = Arrays.copyOf(a,10);
        printArray(a);
    }
    
    public static void main(String[] args) {
        fill_test();
        System.out.println();
        copy_test();
        System.out.println();
        copyOf_test();
    }
}

代码运行结果如下:

1 1 1 1 1 
1 2 3 4 5 
1 2 3 4 5 0 0 0 0 0 

6.包装

以下代码演示了将四个字节包装成一个int以及将一个int分解成四个字节。

package leilei.bit.edu.bigNum;

public class Pack {
    
    public static void pack() {
        byte[] a = {1,2,3,4};
        int result = (a[0] & 0xFF) << 24
                    |(a[1] & 0xFF) << 16
                    |(a[2] & 0xFF) << 8
                    |(a[3] & 0xFF) << 0;
        System.out.println("result is: " + result);
    }
    
    public static void unpack() {
        int x = 16909060;
        byte[] result = {
                (byte)(x >>> 24),
                (byte)(x >>> 16),
                (byte)(x >>> 8),
                (byte)(x >>> 0)
        };
        System.out.print("the unpack result is: ");
        for(int i=0; i<result.length; i++) {
            System.out.print(result[i] + " ");
        }
    }
    
    public static void main(String[] args) {
        pack();
        unpack();
    }

}

让代码run起来

result is: 16909060
the unpack result is: 1 2 3 4