方法
静态方法为什么不能调用非静态成员?
这个需要结合 JVM 的相关知识,主要原因如下:
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作
静态方法和实例方法有何不同?
1、调用方式
在外部调用静态方法时,可以使用 类名.方法名
的方式,也可以使用 对象.方法名
的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。
2、访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
重载和重写的区别
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
重载(Overloading)
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
《Java 核心技术》这本书是这样介绍重载的:
如果多个方法(比如
StringBuilder
的构造方法)有相同的名字、不同的参数, 便产生了重载。StringBuilder sb = new StringBuilder(); StringBuilder sb2 = new StringBuilder("HelloWorld");
编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好(这个过程被称为重载解析(overloading resolution))。
Java 允许重载任何方法, 而不只是构造器方法。
综上:重载就是一个类中同个方法名称根据不同的参数列表处理不同的逻辑。
重写
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
- 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被static
修饰的方法能够被再次声明。 - 构造方法无法被重写
综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
方法的重写要遵循“两同两小一大”(以下内容摘录自《疯狂 Java 讲义》):
- “两同”即方法名相同、形参列表相同;
- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
⭐️ 关于 重写的返回值类型 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
public class Admin {
public String name() {
return "超级管理员";
}
}
public class SuperAdmin extends Admin{
@Override
public String name() {
return "管理员";
}
public Hero hero() {
return new Hero();
}
}
public class SuperSuperAdmin extends SuperAdmin {
public String name() {
return "超级超级管理员";
}
@Override
public SuperAdmin hero() {
return new SuperAdmin();
}
}
==和equals()的区别
==
对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==
比较的是值。 - 对于引用数据类型来说,
==
比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()
方法存在于Object
类中,而Object
类是所有类的直接或间接父类。
Object
类 equals()
方法:
public boolean equals(Object obj) {
return (this == obj);
}
equals()
方法存在两种使用情况:
- 类没有覆盖
equals()
方法 :通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object
类equals()
方法。 - 类覆盖了
equals()
方法 :一般我们都覆盖equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
举个例子:
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true
String
中的 equals
方法是被重写过的,因为 Object
的 equals
方法是比较的对象的内存地址,而 String
的 equals
方法比较的是对象的值。
当创建 String
类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String
对象。
String
类equals()
方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
hashCode()与equals()
面试官可能会问你:“你重写过 hashCode()
和 equals()
么?为什么重写 equals()
时必须重写 hashCode()
方法?”
一个非常基础的问题,面试中的重中之重,然而,很多求职者还是会达不到点子上去。
# hashCode() 有什么用?
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()
定义在 JDK 的 Object
类中,这就意味着 Java 中的任何类都包含有 hashCode()
函数。另外需要注意的是: Object
的 hashCode()
方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
# 为什么要有 hashCode?
我们以“HashSet
如何检查重复”为例子来说明为什么要有 hashCode
?
下面这段内容摘自《Head First Java》:
当你把对象加入
HashSet
时,HashSet
会先计算对象的hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode
值作比较,如果没有相符的hashCode
,HashSet
会假设对象没有重复出现。但是如果发现有相同hashCode
值的对象,这时会调用equals()
方法来检查hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。。这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。
其实, hashCode()
和 equals()
都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap
、HashSet
)中,有了 hashCode()
之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HastSet
的过程)!
我们在前面也提到了添加元素进HastSet
的过程,如果 HashSet
在对比的时候,同样的 hashCode
有多个对象,它会继续使用 equals()
来判断是否真的相同。也就是说 hashCode
帮助我们大大缩小了查找成本。
那为什么不只提供 hashCode()
方法呢?
这是因为两个对象的hashCode
值相等并不代表两个对象就相等。
那为什么两个对象有相同的 hashCode
值,它们也不一定是相等的?
因为 hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode
)。
总结下来就是 :
- 如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode
值相等并且equals()
方法返回true
,我们才认为这两个对象相等。 - 如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
相信大家看了我前面对 hashCode()
和 equals()
的介绍之后,下面这个问题已经难不倒你们了。
# 为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。
思考 :重写 equals()
时没有重写 hashCode()
方法的话,使用 HashMap
可能会出现什么问题?
HashMap
会存入的值无法取出的情况,例子如下:
定义了两个Key对象,它们的id都是1,就好比它们是两把相同的都能打开同一扇门的钥匙。当我们往HashMap
里放k1时,首先会调用Key这个类的hashCode
方法计算它的hash值,随后把k1放入hash值所指引的内存位置。这里调用的仍是Object
类的hashCode
方法(所有的类都是Object
的子类),而Object类
的hashCode
方法返回的hash值其实是k1对象的内存地址(假设是1000)。随后把k1放入hash值所指引的内存位置。
如果我们随后是调用hm.get(k1)
,那么我们会再次调用hashCode
方法(还是返回k1的地址1000),随后根据得到的hash值,能很快地找到k1。
但我们这里的代码是hm.get(k2
),当我们调用Object类的hashCode
方法(因为Key里没定义)计算k2的hash值时,其实得到的是k2的内存地址(假设是2000)。由于k1和k2是两个不同的对象,所以它们的内存地址一定不会相同,也就是说它们的hash值一定不同,这就是我们无法用k2的hash值去拿k1的原因。
public class WithoutHashCode {
public static void main(String[] args) {
Key k1 = new Key(1);
Key k2 = new Key(1);
HashMap hm = new HashMap();
hm.put(k1, "Key with id is 1");
System.out.println(hm.get(k2));
}
}
重写hashCode() 时没有重写equals()
方法的话,使用 HashMap
可能会出现什么问题?
HashMap
也会存在存入的值无法取出的情况。如上的WithoutHashCode
例子,
存k1时,是根据它id的hash值,假设这里是100,把k1对象放入到对应的位置。而取k2时,是先计算它的hash值(由于k2的id也是1,这个值也是100),随后到这个位置去找。
HashMap
是用链地址法来处理冲突,也就是说,在100号位置上,有可能存在着多个用链表形式存储的对象。它们通过hashCode
方法返回的hash值都是100。
这时候hm.get(k2)
和hm.get(k1)
得到的是相同的hash值100,就需要调用Key对象的equals方法来判断两者是否相等了。由于我们在Key对象里没有定义equals
方法,系统就不得不调用Object
类的equals
方法。由于Object
的固有方法是根据两个对象的内存地址来判断,所以k1和k2一定不会相等,例子中的hm.get(k2)
得到的是会是null。
总结 :
equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。- 两个对象有相同的
hashCode
值,他们也不一定是相等的(哈希碰撞)。