编写java高质量程序建议读记(2)

 建议34:构造函数尽量简化

建议35:避免在构造函数中初始化其它类

public class test { 
	public static void main(String[] args) {
		Son son = new Son();
		son.doSomething();
	}
}
// 父类
class Father {
	public Father() {
		new Other();
	}
}
// 相关类
class Other {
	public Other() {
		new Son();
	}
}
// 子类
class Son extends Father {
	public void doSomething() {
		System.out.println("Hi, show me Something!");
	}
}

报StatckOverflowError异常,栈(Stack)内存溢出,因为声明变量son时,调用了Son的无参构造函数,JVM又默认调用了父类的构造函数,接着Father又初始化了Other类,而Other类又调用了Son类,于是一个死循环就诞生了,直到内存被消耗完停止。

建议36:使用构造代码块精简程序

用大括号把多行代码封装在一起,形成一个独立的数据体,实现特定算法的代码集合即为代码块,一般来说代码快不能单独运行的,必须要有运行主体。在Java中一共有四种类型的代码块:
1)普通代码块:就是在方法后面使用"{}"括起来的代码片段,它不能单独运行,必须通过方法名调用执行;
2)静态代码块:在类中使用static修饰并用"{}"括起来的代码片段,用于静态变量初始化或对象创建前的环境初始化。
3)同步代码块:使用synchronized关键字修饰,并使用"{}"括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
4)构造代码块:在类中没有任何前缀和后缀并使用"{}"括起来的代码片段;
一个类中至少有一个构造函数如果没有,编译器会为其创建一个无参构造函数,构造函数是在对象生成时调用的。如果程序中存在构造代码块,无参构造和有参构造,相当于每个构造函数的最前端都被插入了构造代码块,在通过new关键字生成一个实例时会先执行构造代码块,然后再执行其他代码,也就是说:构造代码块会在每个构造函数内首先执行(构造代码块不是在构造函数之前运行的,它依托于构造函数的执行),作用:
1)初始化实例变量(Instance Variable):如果每个构造函数都要初始化变量,可以通过构造代码块来实现。当然也可以通过定义一个方法,然后在每个构造函数中调用该方法来实现,但是要在每个构造函数中都调用该方法,而这就是其缺点,若采用构造代码块的方式则不用定义和调用,会直接由编译器写入到每个构造函数中。
2)初始化实例环境:一个对象必须在适当的场景下才能存在,如果没有适当的场景,则就需要在创建该对象的时候创建次场景,例如在JEE开发中,要产生HTTP Request必须首先建立HTTP Session,在创建HTTP Request时就可以通过构造代码块来检查HTTP Session是否已经存在,不存在则创建。
以上两个场景利用了构造代码块的两个特性:在每个构造函数中都运行和在构造函数中它会首先运行。很好的利用构造代码块的这个特性不仅可以减少代码量,还可以让程序更容易阅读,特别是当所有的构造函数都要实现逻辑,而且这部分逻辑有很复杂时,这时就可以通过编写多个构造代码块来实现。每个代码块完成不同的业务逻辑(当然了构造函数尽量简单,这是基本原则),按照业务顺序一次存放,这样在创建实例对象时JVM就会按照顺序依次执行,实现复杂对象的模块化创建。

建议37:构造代码块会想你所想

public class test { 
	public static void main(String[] args) {
        new Student();
        new Student("张三");
        new Student(10);
        System.out.println("实例对象数量:"+Student.getNumOfObjects());
    }
}
class Student {
    // 对象计数器
    private static int numOfObjects = 0;
    {
        // 构造代码块,计算产生的对象数量
        numOfObjects++;
    }
    public Student() {
    }
    // 有参构造调用无参构造
    public Student(String stuName) {
        this();
    }
    // 有参构造不调用无参构造
    public Student(int stuAge) {
    }
    //返回在一个JVM中,创建了多少实例对象
    public static int getNumOfObjects(){
        return numOfObjects;
    }
}

输出结果为3,如果编译器把构造代码块插入到各个构造函数中,那带有String形参的构造函数就可能有问题,它会调用无参构造,那通过它生成的Student对象就会执行两次构造代码块:一次是无参构造函数调用构造代码块,一次是执行自身的构造代码块,这样的话计算就不准确了。上一建议是说编译器会把构造代码块插入到每一个构造函数中,但是有一个例外的情况没有说明:如果遇到this关键字(也就是构造函数调用自身的其它构造函数时),则不插入构造代码块,对于我们的例子来说,编译器在编译时发现String形参的构造函数调用了无参构造,于是放弃插入构造代码块,所以只执行了一次构造代码块。那super也会类似处理了?其实不会在构造块的处理上,super方法没有任何特殊的地方,编译器只把构造代码块插入到super方法之后执行而已,仅此不同。

建议38:使用静态内部类提高封装性

Java中的嵌套类分为两种:静态内部类(也叫静态嵌套类,Static Nested Class)和内部类(Inner Class)。只有在是静态内部类的情况下才能把static修饰符放在类前,其它任何时候static都是不能修饰类的。内部类的好处,
1)提高封装性:从代码的位置上来讲,静态内部类放置在外部类内,其代码层意义就是,静态内部类是外部类的子行为或子属性,两者之间保持着一定的关系。
2)提高代码的可读性:相关联的代码放在一起,可读性肯定提高了。
3)形似内部,神似外部:静态内部类虽然存在于外部类内,而且编译后的类文件也包含外部类(格式是:外部类+内部类),但是它可以脱离外部类存在,也就说我们仍然可以通过new 内部类名声明一个对象,只是需要导入外部类。内部类而已。可能会觉得外部类和静态内部类之间是组合关系(Composition)了,这是错误的,外部类和静态内部类之间有强关联关系,这仅仅表现在"字面上",而深层次的抽象意义则依类的设计。
静态类内部类和普通内部类区别?
1)静态内部类不持有外部类的引用:在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性(如果是private权限也能访问,这是由其代码位置决定的),其它的则不能访问。
2)静态内部类不依赖外部类:普通内部类与外部类之间是相互依赖关系,内部类实例不能脱离外部类实例,也就是说它们会同生共死,一起声明,一起被垃圾回收,而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类也是可以存在的。
3)普通内部类不能声明static的方法和变量:普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static 修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。

建议39:使用匿名类的构造函数

public class test {   
    public static void main(String[] args) {
        List list1=new ArrayList();
        List list2=new ArrayList(){};
        List list3=new ArrayList(){{}};
        System.out.println(list1.getClass() == list2.getClass());
        System.out.println(list2.getClass() == list3.getClass());
        System.out.println(list1.getClass() == list3.getClass());
	}
}

输出3个false,list1就是ArrayList的实例对象,list2 = new ArrayList(){}:list2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有任何覆写的方法而已,其代码类似于:  class Sub extends ArrayList {    } // 声明和赋值   List list2 = new Sub();。而list3 = new ArrayList(){{}}:class Sub extends ArrayList {
       {          //初始化代码块      }   }  // 声明和赋值    List list3 = new Sub();就是多了一个初始化块而已,起到构造函数的功能,一个类肯定有一个构造函数,而且构造函数的名称和类名相同,匿名类的构造函数就是它的构造函数。当然,一个类中的构造函数块可以是多个,也就是说会出现如下代码:List list4 = new ArrayList(){{} {} {} {} {}}; 
匿名类虽然没有名字,但也是可以有构造函数的,它用构造函数块来代替构造函数,那上面的3个输出就很明显了:虽然父类相同,但是类还是不同的。

建议40:匿名类的构造函数很特殊

匿名类的构造函数特殊处理机制,一般类(也就是显式名字的类)的所有构造函数默认都是调用父类的无参构造函数的,而匿名类因为没有名字,只能由构造代码块代替,也就无所谓有参和无参的构造函数了,它在初始化时直接调用了父类的同参数构造函数,然后再调用了自己的构造代码块。

建议41:让多重继承成为现实

Java中一个类可以多重实现,但不能多重继承,就是说一个类能够同时实现多个接口,但不能同时继承多个类。Java中提供的内部类可以曲折的解决多重继承的问题。内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,多重继承才会成为可能。

建议42:让工具类不可实例化

Java项目中使用的工具类非常多,比如JDK自己的工具类 java.lang.Math、java.util.Collections等。工具类的方法和属性都是静态的,不需要生成实例即可访问,由于不希望被初始化,就设置了构造函数private的访问权限,表示除了类本身之外,谁都不能产生一个实例。

建议43:避免对象的浅拷贝

一个类实现了Cloneable接口就表示它具备了被拷贝的能力。如果覆写clone()方法就会完全具备拷贝能力。拷贝是在内存中运行的, 所以在性能方面比直接通过new生成的对象要快很多,特别是在大对象的生成上,这会使得性能的提升非常显著。但是对象拷贝也有一个比较容易忽略的问题:浅拷贝(Shadow Clone,也叫作影子拷贝)存在对象属性拷贝不彻底的问题。所有类都继承自Object,Object提供了一个对象拷贝的默认方法,即super.clone()方法,但是该方法是有缺陷的,他提供的是一种浅拷贝,也就是说它并不会把对象的所有属性全部拷贝一份,而是有选择的拷贝,它 的拷贝规则如下:
1)基本类型:如果变量是基本类型,则拷贝其值。比如int、float等。
2)对象:如果变量是一个实例对象,则拷贝其地址引用,也就是说此时拷贝出的对象与原有对象共享该实例变量,不受访问权限的控制,这在Java中是很疯狂的,因为它突破了访问权限的定义:一个private修饰的变量,竟然可以被两个不同的实例对象访问,这让java的访问权限体系情何以堪。
3)String字符串:这个比较特殊,拷贝的也是一个地址,是个引用但是在修改时,它会从字符串池(String pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String是一个基本类型。

建议44:推荐使用序列化对象的拷贝

可以通过序列化方式在内存中通过字节流的拷贝来实现,也就是把母对象写到一个字节流中,再从字节流中将其读出来,这样就可以重建一个对象了,该新对象与母对象之间不存在引用共享的问题,也就相当于深拷贝了一个对象。被拷贝的类只要实现Serializable这个标志性接口即可,不需要任何实现,当然serialVersionUID常量还是要加上去的,然后我们就可以通过CloneUtils工具进行对象的深拷贝了,用词方法进行对象拷贝时需要注意两点:
1)对象的内部属性都是可序列化的:如果有内部属性不可序列化,则会抛出序列化异常,这会让调试者很纳闷,生成一个对象怎么回出现序列化异常呢?从这一点考虑,也需要把CloneUtils工具的异常进行细化处理。
2)注意方法和属性的特殊修饰符:比如final,static变量的序列化问题会被引入对象的拷贝中,这点需要特别注意,同时transient变量(瞬态变量,不进行序列化的变量)也会影响到拷贝的效果。当然,采用序列化拷贝时还有一个更简单的方法,即使用Apache下的 commons工具包中SerializationUtils类,直接使用更加简洁。

建议45:覆写equals方法时不要识别不出自己

建议46:equals应该考虑null值情景

null.equalsIgnoreCase方法报错,覆写equals方法遵循的一个原则对称性原则:对于任何引用x和y的情形,如果x.equals(y),把么y.equals(x)也应该返回true。解决就是前面加上非空判断即可。

建议47:在equals中使用getClass进行类型判断

在继承关系中容易出现使用instanceof判断得到非想要的结果,父类引用了instanceof关键字,它是用来判断一个类的实例对象的,这很容易让子类钻空子。想要解决也很简单,使用getClass来代替instanceof进行类型判断。

建议48:覆写equals方法必须覆写hashCode方法

HashMap的底层处理机制是以数组的方式保存Map条目的(Map Entry)的,这其中的关键是这个数组的下标处理机制:依据传入元素hashCode方法的返回值决定其数组的下标,如果该数组位置上已经有Map条目,并且与传入的值相等则不处理,若不相等则覆盖;如果数组位置没有条目,则插入,并加入到Map条目的链表中。同理,检查键是否存在也是根据哈希码确定位置,然后遍历查找键值的。

建议49:推荐覆写toString方法

Java提供的默认toString方法不友好类名+@+hashCode

建议50:使用package-info类为包服务

package-info类,它是专门为本包服务的,说它特殊主要体现在三个方面:
1)它不能随便创建:在一般的IDE中,Eclipse、package-info等文件是不能随便被创建的,会报"Type name is notvalid"错误,类名无效。在Java中变量定义规范中规定如下字符是允许的:字母、数字、下划线,以及那个不怎么写的$符号,不过中划线可不在之列,那么怎么创建这个文件呢?很简单,用记事本创建一个,然后拷贝进去再改一下就成了,更直接的办法就是从别的项目中拷贝过来。
2)它服务的对象很特殊:一个类是一类或一组事物的描述,比如Dog这个类,就是描述"阿黄"的,那package-info这个类描述的是什么呢?它是描述和记录本包信息的。
3)package-info类不能有实现代码:package-info类再怎么特殊也是 一个类,也会被编译成 package-info.class,但是在package-info.java文件不能声明package-info类。   
package-info类还有几个特殊的地方,比如不可以继承,没有接口,没有类间关系(关联、组合、聚合等)等,Java中既然有这么特殊的一个类,那肯定有其特殊的作用了,我们来看看它的特殊作用,主要表现在以下三个方面:声明友好类和包内访问常量:这个比较简单,而且很实用,比如一个包中有很多内部访问的类或常量,就可以统一放到package-info类中,这样很方便,便于集中管理,可以减少友好类到处游走的情况。

建议51:不要主动进行垃圾回收

System.gc这样的调用主动对垃圾进行回收是一个非常危险的动作因为System.gc要停止所有的响应,才能检查内存中是否存在可以回收的对象,这对一个应用系统来说风险极大,如果是一个Web应用,所有的请求都会暂停,等待垃圾回收器执行完毕,若此时堆内存(heap)中的对象少的话还可以接受,一但对象较多(现在的web项目是越做越大,框架、工具也越来越多,加载到内存中的对象当然也就更多了),这个过程非常耗时,可能是0.01秒,也可能是1秒,甚至20秒,这就严重影响到业务的运行了。

建议52:推荐使用String直接量赋值

java中有一个字符串池(也叫作字符串常量池,String pool或String Constant Pool或String Literal Pool),在字符串池中容纳的都是String字符串对象,它的创建机制是这样的:创建一个字符串时,首先检查池中是否有字面值相等的字符串,如果有,则不再创建,直接返回池中该对象的引用,若没有则创建之,然后放到池中,并返回新建对象的引用,
java的intern方法处理后会检查当前对象在对象池中是否存在字面值相同的引用对象,如果有则返回池中的对象,如果没有则放置到对象池中,并返回当前对象。虽然Java的每个对象都保存在堆内存中但是字符串非常特殊,它在编译期已经决定了其存在JVM的常量池(Constant Pool),垃圾回收不会对它进行回收的。

建议53:注意方法中传递的参数要求

replaceAll方法确实需要传递两个String类型的参数,但是它要求第一个参数是正则表达式。

建议54:正确使用String、StringBuffer、StringBuilder

CharSequence接口有三个实现类与字符串有关,String、StringBuffer、StringBuilder,虽然它们都与字符串有关,但其处理机制是不同的。
String类是不可变的量,也就是创建后就不能再修改了,比如创建了一个"abc"这样的字符串对象,那么它在内存中永远都会是"abc"这样具有固定表面值的一个对象,不能被修改,即使想通过String提供的方法来尝试修改,也是要么创建一个新的字符串对象,要么返回自己,比如:String  str = "abc"; String str1 = str.substring(1); 其中str是一个字符串对象,其值是"abc",通过substring方法又重新生成了一个字符串str1,它的值是"bc",也就是说str引用的对象一但产生就永远不会变。为什么上面还说有可能不创建对象而返回自己呢?那是因为采用substring(0)就不会创建对象。JVM从字符串池中返回str的引用,也就是自身的引用。
StringBuffer是一个可变字符串,它与String一样,在内存中保存的都是一个有序的字符序列(char 类型的数组),不同点是StringBuffer对象的值是可改变的,例如:StringBuffer sb = new StringBuffer("a"); sb.append("b");  sb的值在改变,初始化的时候是"a" ,经过append方法后,其值变成了"ab"。这与String类通过 "+" String s = "a"; s = s + "b"; 的区别,字符串变量s初始化时是 "a" 对象的引用,经过加号计算后,s变量就修改为了 “ab” 的引用,但是初始化的 “a” 对象还没有改变,只是变量s指向了新的引用地址,再看看StringBuffer的对象,它的引用地址虽不变,但值在改变。
StringBuffer和StringBuilder基本相同,都是可变字符序列,不同点是:StringBuffer是线程安全的,StringBuilder是线程不安全的,翻翻两者的源代码,就会发现在StringBuffer的方法前都有关键字syschronized,这也是StringBuffer在性能上远远低于StringBuffer的原因。
在性能方面,由于String类的操作都是产生String的对象,而StringBuilder和StringBuffer只是一个字符数组的再扩容而已,所以String类的操作要远慢于StringBuffer 和 StringBuilder。
1)使用String类的场景:在字符串不经常变化的场景中可以使用String类,例如常量的声明、少量的变量运算等;
2)使用StringBuffer的场景:在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装等;
3)使用StringBuilder的场景:在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程的环境中,则可以考虑使用StringBuilder,如SQL语句的拼接,JSON封装等。

建议55:注意字符串的位置

 1)System.out.println(1 + 2 + "apples");2)System.out.println("apples" + 1 + 2);结果为3apples和apples12。加号的处理机制:在使用加号进行计算的表达式中,只要遇到String字符串,则所有的数据都会转换为String类型进行拼接,如果是原始数据,则直接拼接,如是对象,则调用toString方法的返回值然后拼接,在“+” 表达式中,String字符串具有最高优先级。

建议56:自由选择字符串拼接方法

字符串拼接有三种方法:加号、concat方法及StringBuilder或StringBuffer的append方法,其中加号是最常用的。在字符串拼接方式中,StringBuilder的append方法最快,StringBuffer的append方法次之(因为StringBuffer的append方法是线程安全的),其次是concat方法,加号最慢。
(1)"+" 方法拼接字符串:虽然编辑器对字符串的加号做了优化,它会使用StringBuilder的append方法进行追加,不过最终是通过toString方法转换为String字符串的,例子中的"+" 拼接的代码如下代码相同str= new StringBuilder(str).append("c").toString();它与纯粹使用StringBuilder的append方法是不同的:一是每次循环都会创建一个StringBuilder对象,二是每次执行完毕都要调用toString方法将其转换为字符串致使效率降低。

(2)concat方法拼接字符串:我们从源码上看一下concat方法的实现,其整体看上去就是一个数组拷贝,虽然在内存中处理都是原子性操作,速度非常快,不过,最后的return语句,每次concat操作都会创建一个String对象,这就是concat速度慢下来的真正原因。

(3)append方法拼接字符串:StringBuilder的append方法直接由父类AbstractStringBuilder实现整个append方法都在做字符数组处理,加长,然后拷贝数组,这些都是基本的数据处理,没有创建任何对象,所以速度也就最快了。

建议57:推荐在复杂字符串操作中使用正则表达式

统计单词个数

public class test {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        while (input.hasNext()) {
            String str = input.nextLine();
            //正则表达式对象
            Pattern p =  Pattern.compile("\\b\\w+\\b");
            //生成匹配器
            Matcher matcher =p.matcher(str);
            int wordsCount = 0;
            while(matcher.find()){
                wordsCount++;
            }
            System.out.println(str + "单词数:" + wordsCount);
        }
    }
}

"\b" 表示的是一个单词的边界,它是一个位置界定符,一边为字符或数字,另外一边为非字符或数字,例如"A"这样一个输入就有两个边界,即单词"A"的左右位置,这也就说明了为什么要加上"\w"(它表示的是字符或数字)。正则表达式在字符串的查找,替换,剪切,复制,删除等方面有着非凡的作用,特别是面对大量的文本字符需要处理但是正则表达式是一个恶魔,它会使程序难以读懂,想想看,写一个包含^、$、\A、\s、\Q、+、?、()、{}、[]等符号的正则表达式,然后再告诉你这是一个" 这样,这样......"字符串查找,威力巨大,但难以控制。

建议58:强烈建议使用UTF编码

Java文件是通过IDE工具默认创建的,编码格式是GBK。(1)Java文件编码:如果我们使用记事本创建一个.java后缀的文件,则文件的编码格式就是操作系统默认的格式。如果是使用IDE工具创建的,如Eclipse,则依赖于IDE的设置,Eclipse默认是操作系统编码(Windows一般为GBK);(2)Class文件编码:通过javac命令生成的后缀名为.class的文件是UTF-8编码的UNICODE文件,这在任何操作系统上都是一样的,只要是.class文件就会使UNICODE格式。需要说明的是,UTF是UNICODE的存储和传输格式,它是为了解决UNICODE的高位占用冗余空间而产生的,使用UTF编码就意味着字符集使用的是UNICODE。统一编码。

建议59:对字符串持有一种宽容的心态

Java使用的是UNICODE编码,而中文UNICODE字符集来源于GB18030的,GB18030又是从GB2312发展起来,GB2312是一个包含了7000多个字符的字符集,它是按照拼音排序,并且是连续的,之后的GBK、GB18030都是在其基础上扩充而来的,所以要让它们完整的排序也就更难。

建议60:性能考虑,数组是首选

对一个int类型的数组求和,取出所有数组元素并相加,如果是基本类型则使用数组效率是最高的,使用集合则效率次之,集合获取数据要进行拆箱操作。基本类型是在栈内存中操作的,而对象是堆内存中操作的,栈内存的特点是:速度快,容量小;堆内存的特点是:速度慢,容量大从性能上讲,基本类型的处理占优势。其次,在进行求和运算时(或者其它遍历计算)时要做拆箱动作,因此无谓的性能消耗也就产生了。在实际测试中发现:对基本类型进行求和运算时,数组的效率是集合的10倍。

建议61:若有必要,使用变长数组

Java中的数组是定长的,一旦经过初始化声明就不可改变长度,可以通过对数组扩容"婉转" 地解决数组内存不足。比如可以用采用的是Arrays数组工具类的copyOf方法,产生了一个newLen长度的新数组,并把原有的值拷贝了进去,之后就可以对超长的元素进行赋值了(依据类型的不同分别赋值0、false或null)。

建议62:警惕数组的浅拷贝

通过copyOf方法产生的数组是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型是直接拷贝值,其它都是拷贝引用地址。需要说明的是,数组的clone方法也是与此相同的,同样是浅拷贝,而且集合的clone方法也都是浅拷贝。

建议63:在明确的场景下,为集合指定初始容量

ArrayList的扩容原则,那还有一个问题:elementData的默认长度是多少是10,如果我们使用默认方式声明ArrayList,如new ArrayList(),则elementData的初始长度是10,我们看看ArrayList的三个构造函数。ArrayList():默认构造函数,提供初始容量为10的空列表。 ArrayList(int initialCapacity):构造一个具有指定初始容量的空列表。
ArrayList(Collection<? extends E> c):构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。如果不设置初始容量,系统会按照1.5倍的规则扩容,每次扩容都是一次数组的拷贝,如果数据量大,这样的拷贝会非常消耗资源,而且效率非常低下。所以,我们如果知道一个ArrayList的可能长度,然后对ArrayList设置一个初始容量则可以显著提高系统性能。其它的集合如Vector和ArrayList类似,只是扩容的倍数不同而已,Vector扩容2倍。

建议64:多种最值算法,适时选择

(1)自行实现,快速查找最大值不要求排序,只要遍历一遍数组即可找出最大值。(2)先排序,后取值对于求最大值,也可以采用先排序后取值的方式,当数据个数较少时想着差异较小,当数据取第二大时就要求剔除重复元素并升序排列,这都是由TreeSet类实现的,然后可再使用lower方法寻找小于最大值的值。

建议65:避开基本类型数组转换列表陷阱

public class test{
    public static void main(String[] args) {
        int data [] = {1,2,3,4,5};
        List list= Arrays.asList(data);
        System.out.println("列表中的元素数量是:"+list.size());
    }
}

结果为1,因为Arrays.asList的方法:public static <T> List<T> asList(T... a) {      return new ArrayList<>(a); }asList方法输入的是一个泛型变长参数,基本类型是不能泛型化的,也就是说8个基本类型不能作为泛型参数,要想作为泛型参数就必须使用其所对应的包装类型,在Java中,数组是一个对象,它是可以泛型化的,也就是说我们的例子是把一个int类型的数组作为了T的类型,所以在转换后在List中就只有一个类型为int数组的元素了。把int替换为Integer即可让输出元素数量为5。

建议66:asList方法产生的List的对象不可更改

建议67:不同的列表选择不同的遍历算法

ArrayList数组实现了RandomAccess接口(随机存取接口),这样标志着ArrayList是一个可以随机存取的列表。在Java中,RandomAccess和Cloneable、Serializable一样,都是标志性接口,不需要任何实现,只是用来表明其实现类具有某种特质的,实现了Cloneable表明可以被拷贝,实现了Serializable接口表明被序列化了,实现了RandomAccess接口则表明这个类可以随机存取,对我们的ArrayList来说也就标志着其数据元素之间没有关联,即两个位置相邻的元素之间没有相互依赖和索引关系,可以随机访问和存取。迭代器是23中设计模式中的一种,"提供一种方法访问一个容器对象中的各个元素,同时又无须暴露该对象的内部细节",也就是说对于ArrayList,需要先创建一个迭代器容器,然后屏蔽内部遍历细节,对外提供hasNext、next等方法。问题是ArrayList实现RandomAccess接口,表明元素之间本来没有关系,可是,为了使用迭代器就需要强制建立一种相互"知晓"的关系,比如上一个元素可以判断是否有下一个元素,以及下一个元素时什么等关系,这也就是foreach遍历耗时的原因。列表遍历不是那么简单的,适时选择最优的遍历方式,不要固化为一种。

建议68:频繁插入和删除时使用LinkList

(1)插入元素:列表中我们使用最多的是ArrayList,下面来看看它的插入(add方法)算法。arrayCopy方法,只要插入一个元素,其后的元素就会向后移动一位,虽然arrayCopy是一个本地方法,效率非常高,但频繁的插入,每次后面的元素都要拷贝一遍,效率就更低了,特别是在头位置插入元素时。LinkedList是一个双向列表,它的插入只是修改了相邻元素的next和previous引用,其add方法第一步检查是否越界,判断插入元素的位置与列表的长度比较,如果相等,调用linkLast,否则调用linkBefore方法。但这两个方法的共同点都是双向链表插入算法,把自己插入到链表,然后再把前节点的next和后节点的previous指向自己。插入一个元素的过程中,没有任何元素会有拷贝过程,只是引用地址变了,那效率自然就高了。
(2)删除元素:ArrayList提供了删除指定位置上的元素,删除指定元素,删除一个下标范围内的元素集等删除动作。三者的实现原理基本相似,都是找索引位置,然后删除。我们以最常用的删除指定下标的方法(remove方法)。双向链表标准删除算法,没有任何耗时的操作,全部都是引用指针的变更。(3)修改元素:修改元素值,在这一点上LinkedList输给了ArrayList,这是因为LinkedList是按顺序存储的,因此定位元素必然是一个遍历过程,效率大打折扣。LinkedList:删除和插入效率高;ArrayList胜一局:修改元素效率高。

建议69:列表相等只关心元素数据

public class test {
    public static void main(String[] args) {
        ArrayList<String> strs = new ArrayList<String>();
        strs.add("A");       
        Vector<String> strs2 = new Vector<String>();
        strs2.add("A");       
        System.out.println(strs.equals(strs2));
    }
}

true,两者都是列表List都实现了List接口,也都继承了AbstractList抽象类,其equals方法是在AbstractList中定义的。

public boolean equals(Object o) {
            if (o == this)
                return true;
            //是否是列表,注意这里:只要实现List接口即可
            if (!(o instanceof List))
                return false;
            //通过迭代器访问List的所有元素
            ListIterator<E> e1 = listIterator();
            ListIterator e2 = ((List) o).listIterator();
            //遍历两个List的元素
            while (e1.hasNext() && e2.hasNext()) {
                E o1 = e1.next();
                Object o2 = e2.next();
                //只要存在着不相等就退出
                if (!(o1==null ? o2==null : o1.equals(o2)))
                    return false;
            }
            //长度是否也相等
            return !(e1.hasNext() || e2.hasNext());
        }

只要实现了List接口就成,它不关心List的具体实现类,只要所有元素相等,并且长度也相等就表明两个List是相等的,与具体的容量类型无关。也就是说,上面的例子虽然一个是Arraylist,一个是Vector,只要里面的元素相等,那结果也就相等。

建议70:子列表只是原列表的一个视图

List接口提供了subList方法,其作用是返回一个列表的子列表,这与String类subSting有点类似,

public class test {
    public static void main(String[] args) {
        // 定义一个包含两个字符串的列表
        List<String> c = new ArrayList<String>();
        c.add("A");
        c.add("B");
        // 构造一个包含c列表的字符串列表
        List<String> c1 = new ArrayList<String>(c);
        // subList生成与c相同的列表
        List<String> c2 = c.subList(0, c.size());
        // c2增加一个元素
        c2.add("C");
        System.out.println("c==c1? " + c.equals(c1));
        System.out.println("c==c2? " + c.equals(c2));
    }
}

结果为false和true。String类的subString方法,

public static void testStr() {
        String str = "AB";
        String str1 = new String(str);
        String str2 = str.substring(0) + "C";
        System.out.println("str==str1? " + str.equals(str1));
        System.out.println("str==str2? " + str.equals(str2));
    }

输出true和false。subList的方法是由AbstractList实现的,它会根据是不是可以随机存取来提供不同的SubList实现方式,不过,随机存取的使用频率比较高,而且RandomAccessSubList也是subList的子类,所以所有的操作都是由Sublist类实现的(除了自身的SubList方法外),看看SubList类的代码:

class SubList<E> extends AbstractList<E> {
    //原始列表
    private final AbstractList<E> l;
    //偏移量
    private final int offset;
    private int size;
    //构造函数,注意list参数就是我们的原始列表
    SubList(AbstractList<E> list, int fromIndex, int toIndex) {
        /*下标校验代码  略*/
        //传递原始列表
        l = list;
        offset = fromIndex;
        //子列表的长度
        size = toIndex - fromIndex;
        this.modCount = l.modCount;
    }
    //获得制定位置的元素
    public E get(int index) {
        /*下标校验 略*/
        //从原始字符串中获得制定位置的元素
        return l.get(index+offset);
    }
    //增加或插入
    public void add(int index, E element) {
        /*下标校验 略*/
        //直接增加到原始字符串上
        l.add(index+offset, element);
        /*处理长度和修改计数器*/
    }
   /*其它方法 略*/
}

subList方法的实现原理了:它返回的SubList类也是AbstractList的子类,其所有的get、set、add、remove等都是在原始列表上的操作,它自身并没有生成一个新的数组或是链表,也就是子列表只是原列表的一个视图(View)而已。所有的修改动作都映射到了原列表上。

建议71:推荐使用subList处理局部列表

一个列表有100个元素,现在要删除索引位置为20~30的元素所有的操作都是在原始列表上进行的,那我们就用subList先取出一个子列表,然后清空。因为subList返回的list是原始列表的一个视图,删除这个视图中 的所有元素,最终都会反映到原始字符串上,那么一行代码解决问题了。

public static void main(String[] args) {
        // 初始化一个固定长度,不可变列表
        List<Integer> initData = Collections.nCopies(100, 0);
        // 转换为可变列表
        List<Integer> list = new ArrayList<Integer>(initData);
        //删除指定范围内的元素
        list.subList(20, 30).clear();
    }
public void clear() {
        removeRange(0, size());
    }
protected void removeRange(int fromIndex, int toIndex) {
        ListIterator<E> it = listIterator(fromIndex);
        for (int i=0, n=toIndex-fromIndex; i<n; i++) {
            it.next();
            it.remove();
        }
    }

建议72:生成子列表后不要再操作原列表

subList取出的列表是原列表的一个视图,原数据集(代码中的lsit变量)修改了,但是subList取出的子列表不会重新生成一个新列表(这点与数据库视图是不相同的),后面在对子列表继续操作时,就会检测到修改计数器与预期的不相同,于是就抛出了并发修改异常。数据库的一张表可以有多个视图List也可以有多张视图,也就是可以有多个子列表但问题是只要生成的子列表多于一个,任何一个子列表都不能修改了,否则就会抛出ConcurrentModificationException异常。subList生成子列表后,保持原列表的只读状态。

建议73:使用Comparator进行排序

要想给数据排序,有两种实现方式,一种是实现Comparable接口,一种是实现Comparator接口,在JDK中,对Collections.sort方法的解释是按照自然顺序进行升序排列,这种说法其实不太准确的,sort方法的排序方式并不是一成不变的升序,也可能是倒序,这依赖于compareTo的返回值,我们知道如果compareTo返回负数,表明当前值比对比值小,零表示相等,正数表明当前值比对比值大,实现了Comparable接口的类表明自身是可以比较的,有了比较才能进行排序,而Comparator接口是一个工具类接口,它的名字(比较器)也已经表明了它的作用:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑,从这方面来说,一个类可以有很多的比较器,只要有业务需求就可以产生比较器,有比较器就可以产生N多种排序,而Comparable接口的排序只能说是实现类的默认排序算法,一个类稳定、成熟后其compareTo方法基本不会变,也就是说一个类只能有一个固定的、由compareTo方法提供的默认排序算法。Comparable接口可以作为实现类的默认排序算法,Comparator接口则是一个类的扩展排序工具。

建议74:不推荐使用binarySearch对列表进行检索

对一个列表进行检索时使用最多的是indexOf方法,它简单、好用,而且也不会出错,虽然它只能检索到第一个符合条件的值,但是可以生成子列表后再检索,这样也即可以查找出所有符合条件的值了。Collections工具类也提供了一个检索方法,binarySearch该方法也是对一个列表进行检索的,可查找出指定值的索引。

public class test {
    public static void main(String[] args) {
        List<String> cities = new ArrayList<String> ();
        cities.add("上海");
        cities.add("广州");
        cities.add("广州");
        cities.add("北京");
        cities.add("天津");
        //indexOf取得索引值
        int index1= cities.indexOf("广州");
        //binarySearch找到索引值
        int index2= Collections.binarySearch(cities, "广州");
        System.out.println("索引值(indexOf):"+index1);
        System.out.println("索引值(binarySearch):"+index2);
    }
}

使用二分搜索法搜索指定列表,以获得指定对象。其实现的功能与indexOf是相同的,只是使用的是二分法搜索列表,索引值(indexOf):1,     索引值(binarySearch):2indexOf方法就是一个遍历,找到第一个元素值相等则返回二分法查询的一个首要前提是:数据集以实现升序排列,否则二分法查找的值是不准确的。不排序怎么确定是在小区(比中间值小的区域) 中查找还是在大区(比中间值大的区域)中查找呢?二分法查找必须要先排序,这是二分法查找的首要条件。

建议75:集合中的元素必须做到compareTo和equals同步

实现了Comparable接口的元素就可以排序,compareTo方法是Comparable接口要求必须实现的,它与equals方法有关系吗?有关系,在compareTo的返回为0时,它表示的是进行比较的两个元素时相等的。indexOf是通过equals方法判断的,equals方法等于true就认为找到符合条件的元素了,而binarySearch查找的依据是compareTo方法的返回值,返回0即认为找到符合条件的元素了。indexOf依赖equals方法查找,binarySearch则依赖compareTo方法查找;
 equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同。一个决定排序位置,一个是决定相等,我们就应该保证当排序相同时,其equals也相同,否则就会产生逻辑混乱。实现了compareTo方法就应该覆写equals方法,确保两者同步。

建议76:集合运算时使用最优雅方式        

遍历可以实现并集、交集、差集等运算,但是不优雅,1)并集:也叫作合集 list1.addAll(list2); 2)交集:计算两个集合的共有元素list1.retainAll(list2); 3)差集:由所有属于A但不属于B的元素组成的集合list1.removeAll(list2); 4)无重复的并集:并集是集合A加集合B,那如果集合A和集合B有交集,就需要确保并集的结果中只有一份交集,此为无重复的并集list2.removeAll(list1);    //把剩余的list2元素加到list1中      list1.addAll(list2);

建议77:使用shuffle打乱列表

打乱一个列表的顺序,我们不用费尽心思的遍历、替换元素了。我们一般很少用到shuffle这个方法,在什么地方用
1)可用在程序的 "伪装" 上:比如我们例子中的标签云,或者是游侠中的打怪、修行、群殴时宝物的分配策略。
2)可用在抽奖程序中:比如年会的抽奖程序,先使用shuffle把员工顺序打乱,每个员工的中奖几率相等,然后就可以抽出第一名、第二名。
3)可以用在安全传输方面:比如发送端发送一组数据,先随机打乱顺序,然后加密发送,接收端解密,然后进行排序,即可实现即使是相同的数据源,也会产生不同密文的效果,加强了数据的安全性。

建议78:减少HashMap中元素的数量

HashMap和ArrayList的长度都是动态增加的,不过两者的扩容机制不同,HashMap,它在底层是以数组的方式保存元素的,其中每一个键值对就是一个元素,HashMap把键值对封装成了一个Entry对象,然后再把Entry对象放到了数组中。也就是说HashMap比ArrayList多了一次封装,多出了一倍的对象。其中HashMap的扩容机制代码(resize(2 * table.length)这就是扩容核心,在插入键值对时会做长度校验,如果大于或者等于阈值,则数组长度会增大一倍。hashMap的size大于数组的0.75倍时,就开始扩容,ArrayList的扩容策略,它是在小于数组长度的时候才会扩容1.5倍综合来说,HashMap比ArrayList多了一层Entry的底层封装对象,多占用了内存,并且它的扩容策略是2倍长度的递增,同时还会根据阈值判断规则进行判断,因此相对于ArrayList来说,同样的数据,它就会优先内存溢出。

建议79:集合中的哈希码不要重复

列表中查找某值是非常耗费资源的,随机存取的列表是遍历查找,顺序存储的列表是链表查找,或者是Collections的二分法查找最快的还要数以Hash开头的集合(如HashMap、HashSet等类)查找,HashMap的ContainsKey方法public boolean containsKey(Object key) {       //判断getEntry是否为空       return getEntry(key) != null;    }getEntry方法会根据key值查找它的键值对(也就是Entry对象),如果没有找到,则返回null。HashMap的table数组是如何存储元素的1)table数组的长度永远是2的N次幂。2)table数组的元素是Entry类型。3)table数组中的元素位置是不连续的。HashMap是如何插入元素的

public V put(K key, V value) {
        //null键处理
        if (key == null)
            return putForNullKey(value);
        //计算hash码,并定位元素
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            //哈希码相同,并且key相等,则覆盖
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //插入新元素,或者替换哈希的旧元素并建立链表
        addEntry(hash, key, value, i);
        return null;
    }

HashMap每次增加元素时都会先计算其哈希码值,然后使用hash方法再次对hashCode进行抽取和统计,同时兼顾哈希码的高位和低位信息产生一个唯一值,也就是说hashCode不同,hash方法返回的值也不同,之后再通过indexFor方法与数组长度做一次与运算,即可计算出其在数组中的位置,简单的说,hash方法和indexFor方法就是把哈希码转变成数组的下标null值也是可以作为key值的,它的位置永远是在Entry数组中的第一位。哈希运算存在着哈希冲突问题,即对于一个固定的哈希算法f(k),允许出现f(k1)=f(k2),但k1≠k2的情况,也就是说两个不同的Entry,可能产生相同的哈希码,HashMap是如何处理这种冲突问题的呢?答案是通过链表,每个键值对都是一个Entry,其中每个Entry都有一个next变量,也就是说它会指向一个键值对---很明显,这应该是一个单向链表,该链表是由addEntity方法完成的如果新加入的元素的键值对的hashCode是唯一的,那直接插入到数组中,它Entry的next值则为null;如果新加入的键值对的hashCode与其它元素冲突,则替换掉数组中的当前值,并把新加入的Entry的next变量指向被替换的元素,于是一个链表就产生了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值