博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java高级篇整理
阅读量:6238 次
发布时间:2019-06-22

本文共 17204 字,大约阅读时间需要 57 分钟。

面试中常常会问及Java细节相关的问题,而由于往往在平时中不会过多的涉及,所以不能得心应手。而通过阅读课本和网上资料,会有一定的帮助,但纸上得来终觉浅,绝知此事须躬行。以博客的形式梳理思路,通过一行行代码,深入理解Java细节,从而对于Java高级特性进行全面掌握。 Java三大特性 封装 封装三大好处 良好的封装减少耦合 类内部的结构可以自由修改(无须修改客户代码) 可以对成员进行精确控制(年龄范围,性别控制等) 隐藏信息,实现细节(对象的属性私有化) 继承 通过继承(is-a)实现代码的复用。 继承特点 子类拥有父类非private的属性和方法 子类可以拥有自己的属性和方法 子类可以用自己的方式实现父类的方法(重写) 构造器 只能被调用,不能被继承,super()调用父类的构造器。 子类会默认调用父类的构造器。 如果没有默认的父类构造器,子类必须显示调用指定父类的构造器,而且必须在子类构造器中的第一行代码实现调用。 publicclassPerson{protectedString name;protectedintage;protectedString sex; Person(String name){ System.out.println("Person Constrctor-----"+ name); }}publicclassHusbandextendsPerson{privateWife wife; Husband(){super("chenssy"); System.out.println("Husband Constructor..."); }publicstaticvoidmain(String[] args) { Husband husband =newHusband(); }}Output:Person Constrctor-----chenssyHusband Constructor... protected关键字 尽可能隐藏,但是允许子类的成员访问。 指明就类用户而言,他是private,但是对于任何继承与此类的子类而言或者其他任何位于同一个包的类而言,他却是可以访问的。 publicclassPerson{privateString name;privateintage;privateString sex;protectedStringgetName() {returnname; }protectedvoidsetName(String name) {this.name = name; }publicStringtoString(){return"this name is "+ name; }/** 省略其他setter、getter方法 /}publicclassHusbandextendsPerson{privateWife wife;publicStringtoString(){ setName("chenssy");//调用父类的setName();returnsuper.toString();//调用父类的toString()方法}publicstaticvoidmain(String[] args) { Husband husband =newHusband(); System.out.println(husband.toString()); }}Output:thisname is chenssy 向上转型 专用类型向较通用类型转换,所以总是安全的。 可能带来属性和方法的丢失。 publicclassPerson{publicvoiddisplay(){ System.out.println("Play Person..."); }staticvoiddisplay(Person person){ person.display(); }}publicclassHusbandextendsPerson{publicstaticvoidmain(String[] args) { Husband husband =newHusband(); Person.display(husband);//向上转型}} 多态 指向子类的父类引用由于向上转型,只能访问父类中拥有的方法和属性,会丢失子类中存在,而父类中不存真的方法。若子类重写了父类的某些方法,在调用方法的时候,必定会使用子类中定义的方法。(动态连接,动态调用。 publicclassWine{publicvoidfun1(){ System.out.println("Wine 的Fun....."); fun2(); }publicvoidfun2(){ System.out.println("Wine 的Fun2..."); }}publicclassJNCextendsWine{/ * @desc子类重载父类方法 * 父类中不存在该方法,向上转型后,父类是不能引用该方法的 * @parama * @returnvoid /publicvoidfun1(String a){ System.out.println("JNC 的 Fun1..."); fun2(); }/*

  • 子类重写父类方法
  • 指向子类的父类引用调用fun2时,必定是调用该方法 */publicvoidfun2(){ System.out.println("JNC 的Fun2..."); }}publicclassTest{publicstaticvoidmain(String[] args) { Wine a =newJNC(); a.fun1(); }}-------------------------------------------------Output:Wine 的Fun.....JNC 的Fun2... 多态的实现 条件(继承、重写、 向上转型) 实现形式(继承、接口)当子类重写父类的方法被调用时,只有对象继承链中的最末端的方法才会被调用 经典案例分析 publicclassA{publicStringshow(D obj) {return("A and D"); }publicStringshow(A obj) {return("A and A"); } }publicclassBextendsA{publicStringshow(B obj){return("B and B"); }publicStringshow(A obj){return("B and A"); } }publicclassCextendsB{}publicclassDextendsB{}publicclassTest{publicstaticvoidmain(String[] args) { A a1 =newA(); A a2 =newB(); B b =newB(); C c =newC(); D d =newD(); System.out.println("1--"+ a1.show(b));//"A and A"System.out.println("2--"+ a1.show(c));//"A and A"System.out.println("3--"+ a1.show(d));//"A and D"System.out.println("4--"+ a2.show(b));//"B and A"System.out.println("5--"+ a2.show(c));//"B and A"System.out.println("6--"+ a2.show(d));//"A and D"System.out.println("7--"+ b.show(b));//"B and B"System.out.println("8--"+ b.show(c));//"B and B"System.out.println("9--"+ b.show(d));//"A and D" }} 其实在继承链中对象方法的调用存在一个优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。 四舍五入 ROUND_UP:远离零方向舍入。向绝对值最大的方向舍入,只要舍弃位非0即进位。 ROUND_DOWN:趋向零方向舍入。向绝对值最小的方向输入,所有的位都要舍弃,不存在进位情况。 ROUND_CEILING:向正无穷方向舍入。向正最大方向靠拢。若是正数,舍入行为类似于ROUND_UP,若为负数,舍入行为类似于ROUND_DOWN。Math.round()方法就是使用的此模式。 ROUND_FLOOR:向负无穷方向舍入。向负无穷方向靠拢。若是正数,舍入行为类似于ROUND_DOWN;若为负数,舍入行为类似于ROUND_UP。 HALF_UP:最近数字舍入(5进)。这是我们最经典的四舍五入。 HALF_DOWN:最近数字舍入(5舍)。在这里5是要舍弃的。 HAIL_EVEN:银行家舍入法。 使用序列化实现对象的拷贝 clone()方法存在缺陷,并不会将对象的所有属性的全部拷贝过来,而是选择性的拷贝。规则如下 基本类型拷贝其值 对象则拷贝地址引用,也就是说此时新对象与原来对象是公用该实例变量。 String字符串,则拷贝其地址引用。但是在修改时,它会从字符串池中重新生成一个新的字符串,原有对象保持不变。 使用序列化来实现对象的深拷贝。在内存中通过字节流的拷贝是比较容易实现的。把母对象写入到一个字节流中,再从字节流中将其读出来,这样就可以创建一个新的对象了,并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。 publicclassCloneUtils { @SuppressWarnings("unchecked")publicstatic Tclone(T obj){ T cloneObj =null;try{//写入字节流ByteArrayOutputStreamout=newByteArrayOutputStream(); ObjectOutputStream obs =newObjectOutputStream(out); obs.writeObject(obj); obs.close();//分配内存,写入原始对象,生成新对象ByteArrayInputStream ios =newByteArrayInputStream(out.toByteArray()); ObjectInputStream ois =newObjectInputStream(ios);//返回生成的新对象cloneObj = (T) ois.readObject(); ois.close(); }catch(Exception e) { e.printStackTrace(); }returncloneObj; }} 抽象类和接口 抽象类 抽象类不能被实例化 抽象方法必须由子类重写 子类中的抽象方法不能与父类的抽象方法同名。 abstract不能与final并列修饰同一个类 abstract 不能与private、static、final或native并列修饰同一个方法。 接口 接口是用来建立类与类之间的协议,它所提供的只是一种形式,而没有具体的实现 Interface所有方法自动生命为public,当然你可以显示的声明为protected、private,编译会出错! 接口中德成员变量会自动变为为public static final。可以通过类命名直接访问:ImplementClass.name。 实现接口的非抽象类必须要实现该接口的所有方法。抽象类可以不用实现。 抽象类和接口的区别 语法层次 设计层次 抽象层次不同。抽象类是对类进行抽象,接口是对行为的抽象。 跨域不同。抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。我们知道抽象类是从子类中发现公共部分,然后泛化成抽象类,子类继承该父类即可,但是接口不同。实现它的子类可以不存在任何关系,共同之处。is-a和like-a 设计层次不同。抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。 内部类 为何需要内部类 每个内部类都能独立继承一个(接口)实现,无论外围类是否已经继承了某个接口的实现,对于内部类没有影响。 接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。 内部类可以用于多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。 在单个外围类中,可以让多个内部类以不同的方式实现同一接口,或者继承同一个类。 创建内部类对象的时刻并不依赖于外围类对象的创建。 内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。 成员内部类 成员内部类中不能存在任何static的变量和方法 成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。 publicclassOuterClass {privateString str;publicvoidouterDisplay(){ System.out.println("outerClass..."); }publicclassInnerClass{publicvoidinnerDisplay(){//使用外围内的属性str ="chenssy..."; System.out.println(str);//使用外围内的方法outerDisplay(); } }/推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时 /publicInnerClassgetInnerClass(){returnnewInnerClass(); }publicstaticvoidmain(String[] args) { OuterClass outer =newOuterClass(); OuterClass.InnerClass inner = outer.getInnerClass(); inner.innerDisplay(); }}--------------------chenssy...outerClass... 匿名内部类 匿名内部类是没有访问修饰符 匿名内部类是没有构造方法的。因为它连名字都没有何来构造方法。 形参必须是final修饰的。 publicclassOuterClass {publicInnerClassgetInnerClass(finalintnum,String str2){returnnewInnerClass(){intnumber = num +3;publicintgetNumber(){returnnumber; } };/ 注意:分号不能省 /}publicstaticvoidmain(String[] args) { OuterClassout=newOuterClass(); InnerClass inner =out.getInnerClass(2,"chenssy"); System.out.println(inner.getNumber()); }}interfaceInnerClass {intgetNumber();}----------------Output:5 使用匿名内部类 new父类构造器(参数列表)|实现接口() {//匿名内部类的类体部分 }publicabstractclassBird {privateString name;publicStringgetName() {returnname; }publicvoidsetName(String name) {this.name = name; }publicabstractintfly();}publicclassTest {publicvoidtest(Bird bird){ System.out.println(bird.getName() +"能够飞 "+ bird.fly() +"米"); }publicstaticvoidmain(String[] args) { Test test =newTest(); test.test(newBird() {publicintfly() {return10000; }publicStringgetName() {return"大雁"; } }); }}------------------Output:大雁能够飞10000米 使用匿名内部类我们必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口。同时它也是没有class关键字,这是因为匿名内部类是直接使用new来生成一个对象的引用。当然这个引用是隐式的。 匿名内部类注意事项 必须只能继承一个类或实现一个接口 不能定义构造函数 不能存在static变量和方法 属于局部内部类 不能抽象,必须实现所有抽象方法。 使用的形参为何为final 当所在的方法的形参需要被内部类里面使用时,该形参必须为final。 publicclassOuterClass {publicvoiddisplay(final String name,String age){ class InnerClass{voiddisplay(){ System.out.println(name); } } }}java编译后的实际操作如下:publicclassOuterClassInnerClass {publicInnerClass(String name,String age){this.InnerClassname = name;this.InnerClassage = age; }publicvoiddisplay(){ System.out.println(this.InnerClassname +"----"+this.InnerClass$age ); }} 内部类并不是直接调用方法传递的参数,而是利用自身的构造器对传入的参数进行备份,自己内部方法调用的实际上时自己的属性而不是外部方法传递进来的参数。 简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。 故如果定义了一个匿名内部类,并且希望它使用一个其外部定义的参数,那么编译器会要求该参数引用是final的。 匿名内部类初始化 匿名内部类没有构造器,所以使用构造代码块进行初始化。 publicclassOutClass {publicInnerClassgetInnerClass(finalintage,final String name){returnnewInnerClass() {intage_ ; String name_;//构造代码块完成初始化工作{if(0< age && age <200){ age_ = age; name_ = name; } }publicStringgetName() {returnname_; }publicintgetAge() {returnage_; } }; }publicstaticvoidmain(String[] args) { OutClassout=newOutClass(); InnerClass inner_1 =out.getInnerClass(201,"chenssy"); System.out.println(inner_1.getName()); InnerClass inner_2 =out.getInnerClass(23,"chenssy"); System.out.println(inner_2.getName()); }} 实现多重继承 接口 内部类 publicclassFather{publicintstrong(){return9; }}publicclassMother{publicintkind(){return8; }}publicclassSon{/
  • 内部类继承Father类 /classFather_1extendsFather{publicintstrong(){returnsuper.strong() +1; } }classMother_1extendsMother{publicintkind(){returnsuper.kind() -2; } }publicintgetStrong(){returnnewFather_1().strong(); }publicintgetKind(){returnnewMother_1().kind(); }} static Java把内存分为栈内存和堆内存,其中栈内存用来存放一些基本类型的变量、数组和对象的引用,堆内存主要存放一些对象。static所蕴含“静态”的概念表示着它是不可恢复的,即在那个地方,你修改了,他是不会变回原样的,你清理了,他就不会回来了。 static修饰的方法我们称之为静态方法,我们通过类名对其进行直接调用。由于他在类加载的时候就存在了,它不依赖于任何实例,所以static方法必须实现,也就是说他不能是抽象方法abstract。 static局限 只能调用static变量 只能调用static方法 不能以任何形式引用this、super static变量在定义时必须要进行初始化,且初始化时间要早于非静态变量。 强制类型转换 在java中强制类型转换分为基本数据类型和引用数据类型两种。 对于引用数据类型强制转换,一般是将父类转换为子类。 Father father = new Son(); 在这里Son 对象实例被向上转型为father了,但是请注意这个Son对象实例在内存中的本质还是Son类型的,只不过它的能力临时被消弱了而已,如果我们想变强怎么办?将其对象类型还原! Son son = (Son)father; 这条语句是可行的,其实father引用仍然是Father类型的,只不过是将它的能力加强了,将其加强后转交给son引用了,Son对象实例在son的变量的引用下,恢复真身,可以使用全部功能了。 Fatherfather=newFather();Sonson=(Son)father; 这个系统会抛出ClassCastException异常信息. 所以编译器在编译时只会检查类型之间是否存在继承关系,有则通过;而在运行时就会检查它的真实类型,是则通过,否则抛出ClassCastException异常。 所以在继承中,子类可以自动转型为父类,但是父类强制转换为子类时只有当引用类型真正的身份为子类时才会强制转换成功,否则失败。 代码块 分为四种普通代码块、静态代码块(static)、同步代码块(synchronized)、构造代码块。主要说明一下构造代码块。 构造代码块 在类中直接定义没有任何修饰符、前缀、后缀的代码块即为构造代码块。我们明白一个类必须至少有一个构造函数,构造函数在生成对象时被调用。构造代码块和构造函数一样同样是在生成一个对象时被调用 publicclassTest{/*
  • 构造代码 /{ System.out.println("执行构造代码块..."); }/*
  • 无参构造函数 /publicTest(){ System.out.println("执行无参构造函数..."); }/* * 有参构造函数 * @paramid id /publicTest(String id){ System.out.println("执行有参构造函数..."); }} 编译器会将代码块按照他们的顺序(假如有多个代码块)插入到所有的构造函数的最前端,这样就能保证不管调用哪个构造函数都会执行所有的构造代码块。上边的代码等同于下面的格式。 publicclassTest{/*
  • 无参构造函数 /publicTest(){ System.out.println("执行构造代码块..."); System.out.println("执行无参构造函数..."); }/* * 有参构造函数 * @paramid id /publicTest(String id){ System.out.println("执行构造代码块..."); System.out.println("执行有参构造函数..."); }} new一个对象的时候总是先执行构造代码,再执行构造函数,但是有一点需要注意构造代码不是在构造函数之前运行的,它是依托构造函数执行的。 构造函数使用场景。 初始化实例变量。利用编译器将构造代码块添加到每个构造函数中。 初始化实例环境。封装逻辑实现部分。 静态代码块、构造代码块、构造函数执行顺序 他们三者的执行顺序应该为:静态代码块 > 构造代码块 > 构造函数。 publicclassTest{/*
  • 静态代码块 /static{ System.out.println("执行静态代码块..."); }/*
  • 构造代码块 /{ System.out.println("执行构造代码块..."); }/*
  • 无参构造函数 /publicTest(){ System.out.println("执行无参构造函数..."); }/* * 有参构造函数 * @paramid */publicTest(String id){ System.out.println("执行有参构造函数..."); }publicstaticvoidmain(String[] args) { System.out.println("----------------------");newTest(); System.out.println("----------------------");newTest("1"); }}-----------Output:执行静态代码块...----------------------执行构造代码块...执行无参构造函数...----------------------执行构造代码块...执行有参构造函数... equals()方法 超类Object中有这个equals()方法,该方法主要用于比较两个对象是否相等。该方法的源码如下 publicbooleanequals(Object obj) {return(this== obj); } “==”比较两个对象的的内存地址,即若object1.equals(object2)为true,则表示equals1和equals2实际上是引用同一个对象。下面是String的equals()方法。 publicbooleanequals(Object anObject) {if(this== anObject) {returntrue; }if(anObject instanceof String) { String anotherString = (String)anObject;intn = count;if(n == anotherString.count) {charv1[] =value;charv2[] = anotherString.value;inti = offset;intj = anotherString.offset;while(n-- !=0) {if(v1[i++] != v2[j++])returnfalse; }returntrue; } }returnfalse; } 对于这个代码段:if (v1[i++] != v2[j++])return false;我们可以非常清晰的看到String的equals()方法是进行内容比较,而不是引用比较。至于其他的封装类都差不多。 在覆写equals()方法时,一般都是推荐使用getClass来进行类型判断,不是使用instanceof。我们都清楚instanceof的作用是判断其左边对象是否为其右边类的实例,返回boolean类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。 String StringBuffer和String一样都是用来存储字符串的,只不过由于他们内部的实现方式不同,导致他们所使用的范围不同,对于StringBuffer而言,他在处理字符串时,若是对其进行修改操作,它并不会产生一个新的字符串对象,所以说在内存使用方面它是优于String的。 StringBuffer的使用方面,它更加侧重于对字符串的变化,例如追加、修改、删除,相对应的方法. StringBuilder也是一个可变的字符串对象,他与StringBuffer不同之处就在于它是线程不安全的,基于这点,它的速度一般都比StringBuffer快. str+=”b”等同于str=newStringBuilder(str).append(“b”).toString();它变慢的关键原因就在于newStringBuilder()和toString(), final 编译期常量 运行期初始化 final数据、final方法、final类、final参数 publicclassPerson {privateString name; Person(String name){this.name = name; }publicStringgetName() {returnname; }publicvoidsetName(String name) {this.name = name; }}publicclassFinalTest {privatefinal String final_01 ="chenssy";//编译期常量,必须要进行初始化,且不可更改privatefinal String final_02;//构造器常量,在实例化一个对象时被初始化privatestaticRandom random =newRandom();privatefinalintfinal_03 = random.nextInt(50);//使用随机数来进行初始化//引用publicfinal Person final_04 =newPerson("chen_ssy");//final指向引用数据类型FinalTest(String final_02){this.final_02 = final_02; }publicStringtoString(){return"final_01 = "+ final_01 +" final_02 = "+ final_02 +" final_03 = "+ final_03 +" final_04 = "+ final_04.getName(); }publicstaticvoidmain(String[] args) { System.out.println("------------第一次创建对象------------"); FinalTest final1 =newFinalTest("cm"); System.out.println(final1); System.out.println("------------第二次创建对象------------"); FinalTest final2 =newFinalTest("zj"); System.out.println(final2); System.out.println("------------修改引用对象--------------"); final2.final_04.setName("chenssy"); System.out.println(final2); }}------------------Output:------------第一次创建对象------------final_01 = chenssy final_02 = cm final_03 =34final_04 = chen_ssy------------第二次创建对象------------final_01 = chenssy final_02 = zj final_03 =46final_04 = chen_ssy------------修改引用对象--------------final_01 = chenssy final_02 = zj final_03 =46final_04 = chenssy 不要以为某些数据是final就可以在编译期知道其值,通过final_03我们就知道了,在这里是使用随机数其进行初始化,他要在运行期才能知道其值。 异常exception 为何要使用异常 确保我们程序的健壮性,提高系统可用。 异常情形是指阻止当前方法或者作用域继续执行的问题.

异常链,在应用程序中,我们有时候不仅仅只需要封装异常,更需要传递。怎么传递?throws。通过使用异常链,我们可以提高代码的可理解性、系统的可维护性和友好性。 数组 publicclassTest {publicstaticvoidmain(String[] args) {int[] array_00 =newint[10]; System.out.println("一维数组:"+ array_00.getClass().getName());int[][] array_01 =newint[10][10]; System.out.println("二维数组:"+ array_01.getClass().getName());int[][][] array_02 =newint[10][10][10]; System.out.println("三维数组:"+ array_02.getClass().getName()); }}-----------------Output:一维数组:[I二维数组:[[I三维数组:[[[I 性能优先考虑数组。Arrays.copy 属于浅拷贝。 数组转list。arrays.asList(),需要对基本类型进行封装才能正确转换。 “` public static List asList(T… a) { return new ArrayList(a); } public static void main(String[] args) { int[] datas = new int[]{1,2,3,4,5}; List list = Arrays.asList(datas); System.out.println(list.size()); } ————Output: 1 asList返回的是一个长度不可变的列表。数组是多长,转换成的列表是多长,我们是无法通过add、remove来增加或者减少其长度的。 集合大家族 Collection接口 List接口 有序的Collection,实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。 ArrayList ArrayList动态数组,擅长于随机访问。同时ArrayList是非同步的。 LinkedList 双向链表 Vector 同步 Stack set接口 不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。实现了Set接口的集合有:EnumSet、HashSet、TreeSet。 EnumSet 是枚举的专用Set。所有的元素都是枚举类型。 HashSet HashSet堪称查询速度最快的集合,因为其内部是以HashCode来实现的。它内部元素的顺序是由哈希码来决定的,所以它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。 TreeSet 基于TreeMap,生成一个总是处于排序状态的set,内部以TreeMap来实现。它是使用元素的自然顺序对元素进行排序,或者根据创建Set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。 Map接口 实现map的有:HashMap、TreeMap、HashTable、Properties、EnumMap。 HashMap 如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,可能通过查看HashMap.Entry的源码它是一个单链表结构。 TreeMap 键以某种排序规则排序,内部以red-black(红-黑)树数据结构实现,实现了SortedMap接口 HashTable 也是以哈希表数据结构实现的,解决冲突时与HashMap也一样也是采用了散列链表的形式,不过性能比HashMap要低。 Queue hashCode hashCode重要么?不重要,对于List集合、数组而言,他就是一个累赘,但是对于HashMap、HashSet、HashTable而言,它变得异常重要。所以在使用HashMap、HashSet、HashTable时一定要注意hashCode。对于一个对象而言,其hashCode过程就是一个简单的Hash算法的实现,其实现过程对你实现对象的存取过程起到非常重要的作用。 如果我们存放的数据超过了int的范围呢?这样就必定会产生两个相同的index,这时在index位置处会存储两个对象,我们就可以利用key本身来进行判断。所以具有相索引的对象,在该index位置处存在多个对象,我们必须依靠key的hashCode和key本身来进行区分。 如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。 如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。(散列冲突,导致hashCode相同,但是对象不同)。 fail-fast(快速失败) 概念 “快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。 原因 ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。 解决方案 在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。 使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。 CopyOnWriterArrayList所代表的核心概念就是:任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的。 保持compareTo和equals同步 在Java中我们常使用Comparable接口来实现排序,其中compareTo是实现该接口方法。我们知道compareTo返回0表示两个对象相等,返回正数表示大于,返回负数表示小于。同时我们也知道equals也可以判断两个对象是否相等,那么他们两者之间是否存在关联关系呢? 对于compareTo和equals两个方法我们可以总结为:compareTo是判断元素在排序中的位置是否相等,equals是判断元素是否相等,既然一个决定排序位置,一个决定相等,所以我们非常有必要确保当排序位置相同时,其equals也应该相等。 如果你也想在IT行业拿高薪,可以参加我们的训练营课程,选择最适合自己的课程学习,技术大牛亲授,7个月后,进入名企拿高薪。我们的课程内容有:Java工程化、高性能及分布式、高性能、深入浅出。高架构。性能调优、Spring,MyBatis,Netty源码分析和大数据等多个知识点。如果你想拿高薪的,想学习的,想就业前景好的,想跟别人竞争能取得优势的,想进阿里面试但担心面试不过的,你都可以来,群号为: 454377428 注:加群要求 1、具有1-5工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加。 2、在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加。 3、如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的,可以加。 4、觉得自己很牛B,一般需求都能搞定。但是所学的知识点没有系统化,很难在技术领域继续突破的可以加。 5.阿里Java高级大牛直播讲解知识点,分享知识,多年工作经验的梳理和总结,带着大家全面、科学地建立自己的技术体系和技术认知! 6.小号或者小白之类加群一律不给过,谢谢。 目标已经有了,下面就看行动了!记住:学习永远是自己的事情,你不学时间也不会多,你学了有时候却能够使用自己学到的知识换得更多自由自在的美好时光!时间是生命的基本组成部分,也是万物存在的根本尺度,我们的时间在那里我们的生活就在那里!我们价值也将在那里提升或消弭!Java程序员,加油吧

转载地址:http://owuia.baihongyu.com/

你可能感兴趣的文章
关于网站访问出现的以下问题
查看>>
FFmpeg架构之其他重要数据结构的初始化
查看>>
List(二)
查看>>
Discuz论坛黑链清理教程
查看>>
committed access rate(CAR)承诺访问速率
查看>>
我的友情链接
查看>>
c#访问mysql数据库
查看>>
Postfix 邮件路由和传输研究
查看>>
Servlet学习小结
查看>>
“深入剖析WCF的可靠会话”系列[共8篇]
查看>>
装XP-呼唤可信的技术,呼唤可信的盘。
查看>>
中国***江湖之八大门派
查看>>
算法图解-动态规划
查看>>
Nginx 优化
查看>>
大家放弃XP,开始尝鲜吧……
查看>>
yii2 自动写入update_at,create_at字段
查看>>
PXE批量实现自动化安装系统
查看>>
13.组合查询--SQL
查看>>
find命令学习
查看>>
ESXi 5 USB 启动
查看>>