Java开发者常犯的10个错误

Java开发者常犯的10个错误

下面列表总结了Java开发者经常犯的10个错误。

1.将Array转换为ArrayList

将数组转换为ArrayList,经常这样做:

    List<String> list = Arrays.asList(arr);

Arrays.asList()会返回一个ArrayList,但返回的这个ArrayList是Arrays类内部的一个静态私有类,而不是java.util.ArrayList类。java.until.Arrays.ArrayList类有set(),get(),contain()方法,但是没有添加元素的任何方法,所以它的大小是固定的。所以为了创建一个真实的ArrayList,应该:

    ArrayList<String> arrayList = new ArrayList(Arrays.asList(arr));

ArrayList的构造器可以接受一个集合类型,它也是java.util.Arrays.ArrayList的超类。

2 .检查数组是否包含某值

开发时经常这样做:

    Set<String> set = new HashSet<String>(Arrays.asList(arr));
    return set.contains(targetValue);

代码是正常运行的,但是将一个list转换为set是没有必要的。将list转换为set需要花费额外的时间。所以这可以优化为:

    Arrays.asList(arr).contains(targetValue);

    //或者
    for(String s:arr){
        if(s.equals(targetValue))
            return true;
    }
    return false;

当然,第一个比第二个是更可读的。

3. 在循环里面从一个List里移出元素

思考下面这样一段代码,在遍历期间移出元素:

   ArrayLisy<String> list = new ArrayList<String>(Arrays.asList("a","b","c","d"));
    for(int i = 0;i < list.size();i++){
        list.remove(i);
    }
    System.out.print(list);

上面的输出是:

[b,d]

这是一个非常严重的问题。当一个元素移出的时候,list的大小变小,索引改变,所以如果想在循环里通过索引来删除多个元素是不合适的。

在循环中使用iterator遍历器才是删除元素的正确方式。你可能又会想到Java的foreach循环很像一个遍历器,但实际上它不是的。思考下面的代码:

    ArrayList<String> list = new ArrayList<String>(Arrays.asList("a","b","c","d"));

    for(String s:list){
        if(s.equals("a"))
            list.remove(s);
    }

但是上面的代码会抛出 ConcurrentModificationException异常。

再看下面的代码:

    ArrayList<String> list = new ArrayList<String>(Arrays.asList("a","b","c","d"));
    Iterator<String> iter = list.iterator();
    while(iter.hasNext()){
        String s = iter.next();
        if(s.equals("a"))
            list.remove(s);
    }

首先明白抛出ConcurrentModificationException异常的原因。查看ArrayList的源码,可以看到在830和831也就是Iterator的next()方法的开始处,如下:

  public E next() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
                ...
                }

当modCount和expectedModCount不等时就会抛出该异常。那么modCount是何时改变,expectedModCount是何时初始化的呢?分析程序逻辑,可能是remove()引起的modCount改变。定位到remove()源码:

private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

可见调用remove()方法会引起modCount++;而expectedModCount又是在得到迭代器的时候初始化的。所以就造成了ConcurrentModificationException异常。
。在foreach循环中,编译器会在移出元素操作之后调用.next(),这就引起了ConcurrentModificationException异常。
思考下面这段代码:

 ArrayList<String> list = new ArrayList<String>(Arrays.asList("a","b","c","d"));
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()){
            String s = iter.next();
                if(s.equals("a")){
                iter.remove();
                //下面这句将无用
                //list.remove(s);
            }
        }

这段代码结果正确,并且输出也是正确的。调用iter.remove(),来实现移出元素。迭代器的remove()做了什么呢?看源码:

 public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            try {
                //这句会实现将目标元素移出
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                //这句很关键,更新了expectedModCount值
                expectedModCount = modCount;
                limit--;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

可以参考ArrayList和ArrayList.iterator()的源码。

4. Hashtable和HashMap

在算法上,Hashtable是数据结构的名字。但是在Java中数据结构的名字是HashMap。Hashtable和HashMap的关键不同是Hashtable是同步的。所以一般都不会使用Hashtable,而使用HashMap。

5. 使用Collection的原始类型

在Java中,原始类型和无界通配符类型是非常容易混淆的。以Set为例,Set是原始类型,然而Set

    public static void add(List list,Object o){
        list.add(o);
    }

    public static void main(String[] args){
        List<String> list = new ArrayList<String>();
        add(list,10);
    String s = list.get(0);
    }

上面的代码会抛出一个异常:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at ...

当原始类型集合跳过一般类型检验时,使用原始类型集合是危险且不安全的。Set,Set<\?> 和 Set<0bject>的不同:
Set和Set<\?>都能持有任何类型的元素。Set<\?>有如下两方面的含义:
- ?标记代表任何类型,Set<\?>可以持有任何类型的元素
- 由于我们不知道?的类型,所以我们不能put任何元素到Set<\?>集合中

    //合法代码
    public static void main(String[] args) {
        HashSet<Integer> s1 = new HashSet<Integer>(Arrays.asList(1, 2, 3));
        printSet(s1);

        HashSet<String> s2 = new HashSet<String>(Arrays.asList("a", "b", "c"));
        printSet(s2);
    }

    public static void printSet(Set<?> s) {
        for (Object o : s) {
            System.out.println(o);
        }
    }

    //非法代码
    public static void printSet(Set<?> s) {
        s.add(10);//该行是非法的
        for (Object o : s) {
            System.out.println(o);
        }
    }

由于不知道<\?>具体类型,所以不能添加任何东西到Set<\?>,因此也不能使用Set<\?>初始化一个set。比如下面的代码是非法的:

    //非法代码
    Set<?> set = new HashSet<?>();

6 .域可访问性

开发者经常使用public作为类域的修饰符。这样很容易通过直接引用得到域值,但是这是一个非常坏的设计。好的规则是给成员尽可能低的访问权限。

    public, default, protected, and private

7. ArrayList和LinkedList

当开发者不知道二者区别的时候,经常使用ArrayList,因为它看起来更常见。然而,它们之间有一个很大的性能差距。总的来说,如果有大量的aad/remove操作且没有太多的随机访问操作时LinkedList表现得更好。

8. Mutable和Immutable

不可变对象有很多好处,比如简单,安全等。但是它要求每个确切的值有一个单独的对象,太多的对象可能引起大量的垃圾回收。所以在选择可变与不可变时,应该有一个权衡。
一般 ,可变对象常用来避免产生太多的中间对象。一个经典例子是拼接大量的字符串。如果使用不可变的string,就会产生许多的对象,从而立刻引起垃圾回收。浪费了CPU的时间和资源,所以使用一个可变对象才是正确的方案(比如StringBuilder)。
String result=”“;
for(String s: arr){
result = result + s;
}
也有其他适合可变对象的场景。例如 传递可变对象给方法,从而聚合多个结果,而不是通过很多句法。另外一个例子是排序和过滤,当然你可能创建一个方法来负责原始集合并返回排序集合,但是这可能会引起额外的垃圾回收。

9. 超类构造器和子类构造器

父类和子类构造器的关系
上面例子中将父类的默认构造器显式表达出来就好了。

public Super(){
    System.out.println("Super");
}

10. “”还是使用构造器

创建字符串有两种方式:使用双引号和使用构造器

  //1. use double quotes
    String x = "abc";
    //2. use constructor
    String y = new String("abc");

这二者有什么不同呢?看下面这个例子:

 String a = "abcd";
    String b = "abcd";
    System.out.println(a == b);  // True a b指向相同的字面字符串,内存引用是相同的
    System.out.println(a.equals(b)); // True

    String c = new String("abcd");
    String d = new String("abcd");
    System.out.println(c == d);  // False 在堆内存中c和d指向了不同的对象,不同的对象总是有不同的内存引用。
    System.out.println(c.equals(d)); // True

这主要考虑它们在内存中是如何分配的。当相同的字面字符串被多次创建时,只有每个确切值的一个拷贝被存储。这种现象叫做string interning。在Java中所有编译时常量字符串会自动被intern。
字符创创建时的内存分配
运行时,string的interning
string 的interning在运行时也是可以做的,尽管两个字符串都是使用构造器构造的。看下面的代码:

 String c = new String("abcd").intern();
    String d = new String("abcd").intern();
    System.out.println(c == d);  // Now true
    System.out.println(c.equals(d)); // True

由于字面字符串已经是类型化的String,使用构造器会创建额外的不必须的对象。所以如果只需要创建一个String,应该使用双引号方式。如果需要在堆栈中创建一个新的对象,则应该使用构造器方式。

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值