Java基本类型有byte, short , long ,int ,char , double , float,boolean基本类型的比较看似简单,其实涉及的知识还是比较零散的,在JVM体系中,基本类型是存放在堆栈的栈区,栈对于线程来说是私有的变量。而堆存放的是引用所指向的复杂对象。
- 关于Java的调用传递
关于Java的传递网上有很多说基本类型是值传递,引用类型是引用传递。例如看下面的例子:
private static void swap(int a, int b) {
a = 2;
b = 1;
}
int a = 1;
int b = 2;
swap(a, b);
// 输出结果: a = 1, b = 2
System.out.println("a = " + a + ", b = " + b);
这里调用了swap方法,并没修改两个属性的值,说明调用时传递的对象是值。对于引用类型的调用:
private static void swap2(StringBuffer s1, StringBuffer s2) {
s1.append("a");
s2.append("b");
}
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
swap2(s1, s2);
// 输出结果 s1 = a, s2 = b
System.out.println("s1 = " + s1 + ", s2 = " + s2);
说明传递进去的引用是一个变量的副本,但是该副本还是指向的堆上的对象,StringBuffer是一个可变对象,修改原来的对象让原先的引用的对象也改变了值。这就看起来像是引用调用。Java栈上面存放的基本类型和引用类型。由此可以确定:Jvm调用是传递的栈上面的值。
- 关于基本类型的比较问题
在Java中比较有三种比较, == 与 equals与hashCode。== 表示堆对象地址比较,对于所有类的超类Object定义了另外两个与比较相关的方法,equals默认就是==,hashCode就是给某一个对象定义一个哈希值,这个对于哈希表的操作是重要映射关系。默认的hashCode方法返回的是对象的物理地址对应的一个值:
/**
* 同样的对象调用此方法应该返回同一个值,
* 如果两个对象的equals方法相等,那两个对象的
* hashCode也应该返回一样的值,
* 虽然并不强制要求equals不同对象的hashCode一定要不相等,
* 但是应该认识到hashCode尽量不一样可以提升哈希表的性能
*/
public native int hashCode();
关于基本类型的比较:
基本类型在Java类中都有对应的包装类,Short, Integer, Long…等等
Long l1 = new Long(1L);
Long l2 = new Long(1L);
// false
System.out.println(l1 == l2);
因为都是使用的new关键字,虽然两个变量的值相等,但是==比较的是两个对象地址,不相等返回false。
Long l1 = 1L;
Long l2 = 1L;
// true
System.out.println(l1 == l2);
基本类型都会在JVM的常量池内缓存1个字节的数据,这样可以在一定范围内复用这些值,提升效率,但是这样会导致一些问题。
Long l1 = 128L;
Long l2 = 128L;
// false
System.out.println(l1 == l2);
这里返回了false因为128超出了缓存范围,会隐式构造新的对象,两个对象不想等,返回false。
Long l1 = 128L;
Long l2 = 128L;
// true
System.out.println(l1.equals(l2));
但是如果是小数Double或则Float:
Double d1 = 1D;
Double d2 = 1D;
// false
System.out.println(d1 == d2);
因为小数是无限的,因此不会缓存,都会构造一个新的对象。
基本类型的包装类都会重写equals方法,这里比较逻辑上两个对象的值是否相等。我们看看equals的实现:
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
这里调用longValue()就进行了拆箱操作,就是将包装类拆成了基本类型进行比较。基本类型的比较 == 是比较值是否相等。
Long l1 = 128L;
Long l2 = 128L;
// false
System.out.println(l1 == l2);
// true l1 内部拆箱
System.out.println(l1.equals(l2));
// true l1 直接拆箱
System.out.println(l1.longValue() == l2);
// true l1 l2 同时拆箱
System.out.println(l1.longValue() == l2.longValue());
Integer a = 1;
Long b = 1L;
// true
System.out.println(a.intValue() == b);
只要拆箱了之后都是基本类型的比较,int == 会直接让b也拆箱成基本类型比较,逻辑相等。
但是需要注意以下情况:
// false
System.out.println(new Integer(1).equals(new Long(1L)));
可以查看equals源码:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
如果是同类型包装类,即可拆箱,否则仍然比较对象地址。这个问题十分的隐蔽。
== 比较的都是包装类型,比较是否是同一个对象,如果其中有表达式,比较的是数值.是自动拆箱.
equals不仅比较数值,还会比较类型。
- 哈希比较
网上很多资料和面试宝典什么的都强调了重写了equals一定要重写hashCode方法。 因为在集合中,很多时候是首先判断hashCode的值然后才可能去进一步比较equals的值来确认两个对象是否真的相等。很多通过hash映射来存储的运算都会用到类似于hash表的结构,对象的hash值能够找到数组对应的数组索引,对应索引位置可能是一个链表,这就是所谓的拉链法存储结构,链表就是因为对象的hash值相等,但是equals方法不相等,这个对象就会被添加到对应索引位置的链表头部。hashCode就是为了尽量散列的存储,提升hash性能,equals是逻辑上相等。例如HashMap的containsKey方法,寻找一个对象在集合中是否存在:
public boolean containsKey(Object key) {
// 判断该节点是否存在
return getNode(hash(key), key) != null;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 判断hash值对应的索引下有没有链表
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// hash值相等 并且 该对象的地址相等或则该对象equals相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 此处遍历链表,找到该节点即可返回
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// hash值对应索引位置没有链表,直接返回null
return null;
}
由此看到hashCode就是对象存储在一个hash表中的索引,在HashMap内部,这个hash表的索引为(hash & 哈希表长度) ,这样可以尽量让所有对象的hash值分散在哈希表的各个部分,极端情况下,哈希表长度为1,此时的结构就是一个链表。查询的时间复杂度接近链表,最理想情况下,该结构为一个数组,查询性能O(1)。
基本类型会重写hashCode和equals,这样在存储的时候可以像基本类型一样的去比较。例如下面的例子:
public static class User {
private Integer id;
private String name;
// setter getter
}
User user1 = new User(1, "micro");
User user2 = new User(1, "micro");
Set<User> set = new HashSet<User>();
set.add(user1);
// false
System.out.println(set.contains(user2));
因为默认的hashCode两个对象肯定是不同的,内部映射不到数组的位置,直接就返回false。重写hashCode:
public static class User {
private Integer id;
private String name;
// setter getter
@Override
public int hashCode() {
return this.id;
}
}
比较依旧返回false,因为虽然找到了对应的链表,但是equals比较依旧会调用Object的默认==,返回false, 这样依然不能判断该对象相等,因此这里也需要重写equals。重写了equals一定要重写hashCode,否则equals方法写了等于白写,hashCode负责映射对应的数组索引,equals比较对应索引位置的链表值比较。当然只重写了hashCode也不行,只是找到了对应的索引位置,但是却比较不出对应对象的逻辑相等与否。
public static class User {
private Integer id;
private String name;
// ...
@Override
public int hashCode() {
return this.id;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof User) {
User user = null;
// 名字和id相等才逻辑相等
return this.id == (user = (User) obj).getId() && this.name.equals(user.getName());
}
return false;
}
默认id相等即逻辑相等
User user1 = new User(1, "micro");
User user2 = new User(1, "micro");
Set<User> set = new HashSet<User>();
set.add(user1);
// true
System.out.println(set.contains(user2));
hashCode应该尽量平均分布在整个数组,这样可以提升查询性能,但是equals相等的对象对应的hashCode应该要相等,反之则不然。
- 基本类型的hashCode都是被重写了,并且保证equals相等的对象一定hashCode相等,这样在哈希存储的情况下,才能正常运行。基本类型的处理还需要注意拆箱和常量池缓存等操作,基本类型的传递也是值传递,但是包装类传递的是引用。