面试中常常会问及Java细节相关的问题,而由于往往在平时中不会过多的涉及,所以不能得心应手。而通过阅读课本和网上资料,会有一定的帮助,但纸上得来终觉浅,绝知此事须躬行。以博客的形式梳理思路,通过一行行代码,深入理解Java细节,从而对于Java高级特性进行全面掌握。
Java三大特性
封装
- 封装三大好处
- 良好的封装减少耦合
- 类内部的结构可以自由修改(无须修改客户代码)
- 可以对成员进行精确控制(年龄范围,性别控制等)
- 隐藏信息,实现细节(对象的属性私有化)
继承
通过继承(is-a)实现代码的复用。
- 继承特点
- 子类拥有父类非private的属性和方法
- 子类可以拥有自己的属性和方法
- 子类可以用自己的方式实现父类的方法(重写)
- 构造器
- 只能被调用,不能被继承,super()调用父类的构造器。
- 子类会默认调用父类的构造器。
- 如果没有默认的父类构造器,子类必须显示调用指定父类的构造器,而且必须在子类构造器中的第一行代码实现调用。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- protected关键字
- 尽可能隐藏,但是允许子类的成员访问。
- 指明就类用户而言,他是private,但是对于任何继承与此类的子类而言或者其他任何位于同一个包的类而言,他却是可以访问的。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 向上转型
- 专用类型向较通用类型转换,所以总是安全的。
- 可能带来属性和方法的丢失。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
多态
指向子类的父类引用由于向上转型,只能访问父类中拥有的方法和属性,会丢失子类中存在,而父类中不存真的方法。若子类重写了父类的某些方法,在调用方法的时候,必定会使用子类中定义的方法。(动态连接,动态调用。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 多态的实现
- 条件(继承、重写、 向上转型)
- 实现形式(继承、接口)当子类重写父类的方法被调用时,只有对象继承链中的最末端的方法才会被调用
- 经典案例分析
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
其实在继承链中对象方法的调用存在一个优先级: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字符串,则拷贝其地址引用。但是在修改时,它会从字符串池中重新生成一个新的字符串,原有对象保持不变。
使用序列化来实现对象的深拷贝。在内存中通过字节流的拷贝是比较容易实现的。把母对象写入到一个字节流中,再从字节流中将其读出来,这样就可以创建一个新的对象了,并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
抽象类和接口
抽象类
- 抽象类不能被实例化
- 抽象方法必须由子类重写
- 子类中的抽象方法不能与父类的抽象方法同名。
- abstract不能与final并列修饰同一个类
- abstract 不能与private、static、final或native并列修饰同一个方法。
接口
接口是用来建立类与类之间的协议,它所提供的只是一种形式,而没有具体的实现
- Interface所有方法自动生命为public,当然你可以显示的声明为protected、private,编译会出错!
- 接口中德成员变量会自动变为为public static final。可以通过类命名直接访问:ImplementClass.name。
- 实现接口的非抽象类必须要实现该接口的所有方法。抽象类可以不用实现。
抽象类和接口的区别
- 语法层次
- 设计层次
- 抽象层次不同。抽象类是对类进行抽象,接口是对行为的抽象。
- 跨域不同。抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。我们知道抽象类是从子类中发现公共部分,然后泛化成抽象类,子类继承该父类即可,但是接口不同。实现它的子类可以不存在任何关系,共同之处。is-a和like-a
- 设计层次不同。抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
内部类
为何需要内部类
每个内部类都能独立继承一个(接口)实现,无论外围类是否已经继承了某个接口的实现,对于内部类没有影响。
接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。
- 内部类可以用于多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
- 在单个外围类中,可以让多个内部类以不同的方式实现同一接口,或者继承同一个类。
- 创建内部类对象的时刻并不依赖于外围类对象的创建。
- 内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
成员内部类
- 成员内部类中不能存在任何static的变量和方法
- 成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
匿名内部类
- 匿名内部类是没有访问修饰符
- 匿名内部类是没有构造方法的。因为它连名字都没有何来构造方法。
- 形参必须是final修饰的。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 使用匿名内部类
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
使用匿名内部类我们必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口。同时它也是没有class关键字,这是因为匿名内部类是直接使用new来生成一个对象的引用。当然这个引用是隐式的。
- 匿名内部类注意事项
- 必须只能继承一个类或实现一个接口
- 不能定义构造函数
- 不能存在static变量和方法
- 属于局部内部类
- 不能抽象,必须实现所有抽象方法。
- 使用的形参为何为final
当所在的方法的形参需要被内部类里面使用时,该形参必须为final。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
内部类并不是直接调用方法传递的参数,而是利用自身的构造器对传入的参数进行备份,自己内部方法调用的实际上时自己的属性而不是外部方法传递进来的参数。
简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。 故如果定义了一个匿名内部类,并且希望它使用一个其外部定义的参数,那么编译器会要求该参数引用是final的。
- 匿名内部类初始化
- 匿名内部类没有构造器,所以使用构造代码块进行初始化。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
实现多重继承
- 接口
- 内部类
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
static
Java把内存分为栈内存和堆内存,其中栈内存用来存放一些基本类型的变量、数组和对象的引用,堆内存主要存放一些对象。static所蕴含“静态”的概念表示着它是不可恢复的,即在那个地方,你修改了,他是不会变回原样的,你清理了,他就不会回来了。
static修饰的方法我们称之为静态方法,我们通过类名对其进行直接调用。由于他在类加载的时候就存在了,它不依赖于任何实例,所以static方法必须实现,也就是说他不能是抽象方法abstract。
- static局限
- 只能调用static变量
- 只能调用static方法
- 不能以任何形式引用this、super
- static变量在定义时必须要进行初始化,且初始化时间要早于非静态变量。
强制类型转换
在java中强制类型转换分为基本数据类型和引用数据类型两种。
对于引用数据类型强制转换,一般是将父类转换为子类。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 1
- 2
这个系统会抛出ClassCastException异常信息.
- 所以编译器在编译时只会检查类型之间是否存在继承关系,有则通过;而在运行时就会检查它的真实类型,是则通过,否则抛出ClassCastException异常。
- 所以在继承中,子类可以自动转型为父类,但是父类强制转换为子类时只有当引用类型真正的身份为子类时才会强制转换成功,否则失败。
代码块
分为四种普通代码块、静态代码块(static)、同步代码块(synchronized)、构造代码块。主要说明一下构造代码块。
构造代码块
在类中直接定义没有任何修饰符、前缀、后缀的代码块即为构造代码块。我们明白一个类必须至少有一个构造函数,构造函数在生成对象时被调用。构造代码块和构造函数一样同样是在生成一个对象时被调用
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
编译器会将代码块按照他们的顺序(假如有多个代码块)插入到所有的构造函数的最前端,这样就能保证不管调用哪个构造函数都会执行所有的构造代码块。上边的代码等同于下面的格式。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
new一个对象的时候总是先执行构造代码,再执行构造函数,但是有一点需要注意构造代码不是在构造函数之前运行的,它是依托构造函数执行的。
构造函数使用场景。
- 初始化实例变量。利用编译器将构造代码块添加到每个构造函数中。
- 初始化实例环境。封装逻辑实现部分。
静态代码块、构造代码块、构造函数执行顺序
他们三者的执行顺序应该为:静态代码块 > 构造代码块 > 构造函数。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
equals()方法
超类Object中有这个equals()方法,该方法主要用于比较两个对象是否相等。该方法的源码如下
- 1
- 2
- 3
- 1
- 2
- 3
“==”比较两个对象的的内存地址,即若object1.equals(object2)为true,则表示equals1和equals2实际上是引用同一个对象。下面是String的equals()方法。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
对于这个代码段:if (v1[i++] != v2[j++])return false;我们可以非常清晰的看到String的equals()方法是进行内容比较,而不是引用比较。至于其他的封装类都差不多。
在覆写equals()方法时,一般都是推荐使用getClass来进行类型判断,不是使用instanceof。我们都清楚instanceof的作用是判断其左边对象是否为其右边类的实例,返回boolean类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。
String
StringBuffer和String一样都是用来存储字符串的,只不过由于他们内部的实现方式不同,导致他们所使用的范围不同,对于StringBuffer而言,他在处理字符串时,若是对其进行修改操作,它并不会产生一个新的字符串对象,所以说在内存使用方面它是优于String的。
StringBuffer的使用方面,它更加侧重于对字符串的变化,例如追加、修改、删除,相对应的方法.
StringBuilder也是一个可变的字符串对象,他与StringBuffer不同之处就在于它是线程不安全的,基于这点,它的速度一般都比StringBuffer快.
- 1
- 2
- 3
- 1
- 2
- 3
final
- 编译期常量
- 运行期初始化
final数据、final方法、final类、final参数
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
不要以为某些数据是final就可以在编译期知道其值,通过final_03我们就知道了,在这里是使用随机数其进行初始化,他要在运行期才能知道其值。
异常exception
为何要使用异常
确保我们程序的健壮性,提高系统可用。
异常情形是指阻止当前方法或者作用域继续执行的问题.
异常链,在应用程序中,我们有时候不仅仅只需要封装异常,更需要传递。怎么传递?throws。通过使用异常链,我们可以提高代码的可理解性、系统的可维护性和友好性。
数组
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
性能优先考虑数组。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也应该相等。