Java基础知识点(一)

一、Comparable和Comparator区别

参考资料

java Comparable 和Comparator详解及 区别(附代码)

1. Java比较器的使用背景

Java中的对象,正常情况下,只能比较:== 或 !=,不能使用 > 或者 <;但在实际开发场景中,我们需要对多个对象进行排序,也就需要比较对象的大小(本质上是对象一些属性的大小比较,即基本数据类型的比较,但我们需要制定比较的规则)。这时就需要Java比较器,有两个接口供使用:Comparable 和 Comparator

2. 自然排序:使用Comparable接口

Comparable是在集合内部定义的方法实现的排序,位于java.lang下,Comparable 接口仅仅只包括一个函数,它的定义如下:

package java.lang;
import java.util.*;
 
 
public interface Comparable<T> {
    
    public int compareTo(T o);
}

(1)像String、包装类等实现了Comparable接口,重写了compareTo(obj)方法,给出了比较两个对象大小的方式;

(2)像String、包装类重写compareTo()方法后,进行了从小到大的排序;

(3)重写compareTo()的规则:

  • 如果当前对象this大于形参对象obj,返回正整数;
  • 如果当前对象this小于形参对象obj,返回负整数;
  • 如果当前对象this等于形参对象obj,返回零;

(4)对于自定义类来说,如果需要排序,我们可以让自定义类实现Comparable接口,重写compareTo(obj)方法,在compareTo(obj)中指明如何排序;

(5)在用Collections类的sort方法排序时若不指定Comparator,那就以自然顺序排序。所谓自然顺序就是实现Comparable接口设定的排序方式;

(6)一个已经实现comparable的类的对象或数据,可以通过**Collections.sort(list) 或者Arrays.sort(arr)实现排序。通过Collections.sort(list,Collections.reverseOrder())**对list进行倒序排列。

3. 定制排序:使用Comparator接口

Comparator是在集合外部实现的排序,位于java.util下,Comparator接口包含了两个函数:

package java.util;
public interface Comparator<T> {
 
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

(1)背景:为什么会出现定制排序?

比如我们想让String对象从大到小排列,但String中是从小到大排序;或者JDK中某个类没有实现Comparable接口,但是我们希望对该类对象进行排序,就需要临时定义一种排序方式。

当元素类型没有实现java.lang.Comprable接口而又不方便修改代码,或者实现了该接口的排序规则不适合当前操作,就可以考虑使用Comparator的对象来排序。

(2)重写compare(Object o1, Object o2)方法,比较o1和o2的大小:

  • 如果方法返回正整数,则表示o1大于o2;
  • 如果返回负整数,表示o1小于o2;
  • 返回零,表示相等;

(3)Comparator体现了一种策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为;

(4)通过**Collections.sort(list, Comparator com) 或者Arrays.sort(arr, Comparator com)**实现排序;

4. 对比

Comparable接口的方式一旦一定,保证Comparable接口实现类的对象在任何位置都可以比较大小(一劳永逸);

Comparator接口属于临时性的比较(灵活);

Comparable是排序接口,若一个类实现了Comparable接口,就意味着该类是支持排序的;

Comparator是比较器,我们若需要控制某个类的次序,可以建立一个该类的比较器来进行排序;

一个评论中总结的:comparable是需要比较的对象来实现接口。这样对象调用实现的方法来比较。对对象的耦合度高(需要改变对象的内部结构,破坏性大)。comparator相当于一通用的比较工具类接口。需要定制一个比较类去实现它,重写里面的compare方法,方法的参数即是需要比较的对象。对象不用做任何改变。解耦

二、Java中方法的参数传递机制

参考资料

深入理解Java中方法的参数传递机制

java中方法的参数传递机制

java中方法的参数传递机制

问:当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

答:是值传递,**Java编程语言只有值传递参数。**当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象引用的一个副本,指向同一个对象,对象的内容可以在被调用的方法中改变,但对象的引用(不是引用的副本)是永远不会改变的。

1. 概念

值传递:在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

引用传递:在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

值传递引用传递
根本区别会创建副本不会创建副本
所以函数中无法改变原始对象函数中可以改变原始对象

下面结合生活中的场景,再来深入理解一下值传递和引用传递:

你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。

你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。

但是,不管上面哪种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了,肯定会影响到你。

2. Java中的值传递

  • 如果参数是基本数据类型,那么传过来的就是这个参数的一个副本,也就是这个原始参数的值,这个跟“传值”是一样的(相对于其他语言中的“传址”),如果在方法(函数)中改变了副本的值,不会改变原始的值;
  • 如果参数类型是引用类型(对象),那么传过来的就是这个引用参数的副本,这个副本存放的是参数的地址(因为本身这个参数的变量存放的也是参数的地址),如果在函数中没有改变这个副本的地址,而是改变了地址中的值,那么在函数内的改变会影响到传入的参数。如果在函数中改变了副本的地址,如new一个,那么副本就指向了一个新的地址,此时传入的参数还是指向原来的 地址,所以不会改变参数的值。
  • 总之,不管传递的是什么类型的参数,都是传递的副本。原始类型就是传递值的副本,引用类型就是传递地址的副本,如果在方法内修改了地址指向的内容,那么就会影响传入地址的内容(浅拷贝);但如果在方法内new了一个新指向,副本指向了新的内容,那么副本修改不会改变原地址的内容(深拷贝)。

3. 结合Java内存模型理解

首先看一个例子:

public class Main {
 
    public static void main(String[] args) {
        Man a = new Man("a", 65);
        Man b = new Man("b", 66);
        Man.swap(a, b);
        System.out.println(a + "\n" + b);
    }
}

public class Man {
    private String name;
    private Integer age;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
 
    @Override
    public String toString() {
        return "Man{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
 
    public Man(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
 
    public static void swap(Man x, Man y) {
        Man temp = y;
        y = x;
        x = temp;
    }
}

运行结果为:

Man{name = 'a', age = '65'}
Man{name = 'b', age = '66'}

对象a和b的引用并没有改变;

说明:

img

swap中,a b 向 x y 传递的是值,是 a b 在栈中的值,此处的 传递=拷贝;

也就是说,a b 与 x y 除了类型一致外、值相等(指向了堆的同一地址),没有任何相同处 ;

a b与x y完完全全不相干;

都不相干了,x y 不管这么改变自身的值 ,与 a b 有什么关系呢;

所以, 方法退出,a b 的引用并没有发生变化。

因此,Java中的参数传递是值传递

三、Java的深拷贝和浅拷贝的区别

参考资料

java中的深拷贝与浅拷贝

浅拷贝和深拷贝(谈谈java中的clone)

Java深入理解深拷贝和浅拷贝区别

1. Java中的对象创建

在java语言中,有几种方式可以创建对象呢?

  • 使用new操作符创建一个对象

  • 使用clone方法复制一个对象

那么这两种方式有什么相同和不同呢?

new操作符的本意是分配内存。程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象;

clone顾名思义就是复制, 在Java语言中, clone方法被对象调用会复制对象。所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象。clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域, 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。

2. 复制对象 or 复制引用

如下代码:

Person p = new Person(23, "zhang");  
Person p1 = p;  

System.out.println(p);  
System.out.println(p1); 

当Person p1 = p 执行之后,是创建了一个新的对象吗? 首先看打印结果:

com.pansoft.zhangjg.testclone.Person@2f9ee1ac

com.pansoft.zhangjg.testclone.Person@2f9ee1ac

可已看出,打印的地址值是相同的,既然地址都是相同的,那么肯定是同一个对象。p和p1只是引用而已,他们都指向了一个相同的对象Person(23, “zhang”) 。 可以把这种现象叫做引用的复制。上面代码执行完成之后,内存中的情景为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-70PXlTiP-1602746885206)(http://www.2cto.com/uploadfile/Collfiles/20140120/20140120090020285.jpg)]

如果要真正克隆一个对象:

Person p = new Person(23, "zhang");  
Person p1 = (Person) p.clone();  
  
System.out.println(p);  
System.out.println(p1);  

从打印结果可以看出,两个对象的地址是不同的,也就是说创建了新的对象, 而不是把原对象的地址赋给了一个新的引用变量:

com.pansoft.zhangjg.testclone.Person@2f9ee1ac
com.pansoft.zhangjg.testclone.Person@67f1fba0

以上代码执行完成后, 内存中的情景如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ct795OJB-1602746885207)(http://www.2cto.com/uploadfile/Collfiles/20140120/20140120090020286.jpg)]

而对于对象的引用,又可以分为浅拷贝和深拷贝。

3. 浅拷贝 or 深拷贝

(1)浅拷贝

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值如果属性是内存地址(引用类型),拷贝的就是内存地址 ;因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

上面的示例代码中,Person中有两个成员变量,分别是name和age, name是String类型, age是int类型。示例代码如下所示:

实现对象拷贝的类,必须实现Cloneable接口,并重写**clone()**方法。

Persion类:

public class Person implements Cloneable{  
      
    private int age ;  
    private String name;  
      
    public Person(int age, String name) {  
        this.age = age;  
        this.name = name;  
    }  
      
    public Person() {}  
  
    public int getAge() {  
        return age;  
    }  
  
    public String getName() {  
        return name;  
    }  
      
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return (Person)super.clone();  
    }  
}  

下面通过代码进行clone()的功能验证。如果两个Person对象的name的地址值相同, 说明两个对象的name都指向同一个String对象,也就是浅拷贝, 而如果两个对象的name的地址值不同, 那么就说明指向不同的String对象, 也就是在拷贝Person对象的时候, 同时拷贝了name引用的String对象, 也就是深拷贝。验证代码如下:

Person p = new Person(23, "zhang");  
Person p1 = (Person) p.clone();  
  
System.out.println("p.getName().hashCode() : " + p.getName().hashCode());  
System.out.println("p1.getName().hashCode() : " + p1.getName().hashCode());  
  
String result = p.getName().hashCode() == p1.getName().hashCode()   
        ? "clone是浅拷贝的" : "clone是深拷贝的";  
  
System.out.println(result);  

打印结果为:

p.getName().hashCode() : 115864556
p1.getName().hashCode() : 115864556
clone是浅拷贝的

所以,clone方法执行的是浅拷贝, 在编写程序时要注意这个细节。

由于age是基本数据类型, 那么对它的拷贝没有什么疑议,直接将一个4字节的整数值拷贝过来就行。但是name是String类型的, 它只是一个引用, 指向一个真正的String对象,那么对它的拷贝有两种方式: 直接将源对象中的name的引用值拷贝给新对象的name字段, 或者是根据原Person对象中的name指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的Person对象的name字段。这两种拷贝方式就分别叫做浅拷贝和深拷贝。深拷贝和浅拷贝的原理如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-limO0eGo-1602746885209)(http://www.2cto.com/uploadfile/Collfiles/20140120/20140120090020287.jpg)]

(2)深拷贝

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

现在为了要在clone对象时进行深拷贝, 那么就要Clonable接口,覆盖并实现clone方法,除了调用父类中的clone方法得到新的对象, 还要将该类中的引用变量也clone出来。如果只是用Object中默认的clone方法,是浅拷贝的

static class Body implements Cloneable{  
    public Head head;  
      
    public Body() {}    
    public Body(Head head) {this.head = head;}  
  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return super.clone();  
    }  
}  

static class Head /*implements Cloneable*/{  
    public  Face face;  
      
    public Head() {}     
    public Head(Face face){this.face = face;}  
}   

public static void main(String[] args) throws CloneNotSupportedException {  
      
    Body body = new Body(new Head());       
    Body body1 = (Body) body.clone();  
      
    System.out.println("body == body1 : " + (body == body1) );  
      
    System.out.println("body.head == body1.head : " +  (body.head == body1.head));  
}  

在以上代码中, 有两个主要的类, 分别为Body和Head, 在Body类中, 组合了一个Head对象。当对Body对象进行clone时, 它组合的Head对象只进行浅拷贝。打印结果可以验证该结论:

body == body1 : false

body.head == body1.head : true

如果要使Body对象在clone时进行深拷贝, 那么就要在Body的clone方法中,将源对象引用的Head对象也clone一份。

static class Body implements Cloneable{  
    public Head head;  
    public Body() {}  
    public Body(Head head) {this.head = head;}  
  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        Body newBody =  (Body) super.clone();  
        newBody.head = (Head) head.clone();  
        return newBody;  
    }  
      
}  
static class Head implements Cloneable{  
    public  Face face;  
      
    public Head() {}  
    public Head(Face face){this.face = face;}  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return super.clone();  
    }  
}   
public static void main(String[] args) throws CloneNotSupportedException {  
      
    Body body = new Body(new Head());  
      
    Body body1 = (Body) body.clone();  
      
    System.out.println("body == body1 : " + (body == body1) );  
      
    System.out.println("body.head == body1.head : " +  (body.head == body1.head));  
      
      
}  

打印结果为:

body == body1 : false

body.head == body1.head : false

由此可见,body和body1内的head引用指向了不同的Head对象,也就是说在clone Body对象的同时,也拷贝了它所引用的Head对象, 进行了深拷贝。

(3)彻底的深拷贝

上述深拷贝只是对Body实现了深拷贝,使得Body对象内所引用的其他对象(目前只有Head)都进行了拷贝,也就是说两个独立的Body对象内的head引用已经指向了独立的两个Head对象。但实际上每个Head对象中的组合的Face对象对于Head来说仍然是浅拷贝:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qfOpK89I-1602746885211)(http://www.2cto.com/uploadfile/Collfiles/20140120/20140120090021288.jpg)]

这就说明,两个Body对象还是有一定的联系,并没有完全的独立。这应该说是一种不彻底的深拷贝。

怎样才能保证两个Body对象完全独立呢?

只要在拷贝Head对象的时候,也将Face对象拷贝一份就可以了。这需要让Face类也实现Cloneable接口,实现clone方法,并且在在Head对象的clone方法中,拷贝它所引用的Face对象。修改的部分代码如下:

static class Head implements Cloneable{  
    public  Face face;  
      
    public Head() {}  
    public Head(Face face){this.face = face;}  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        //return super.clone();  
        Head newHead = (Head) super.clone();  
        newHead.face = (Face) this.face.clone();  
        return newHead;  
    }  
}   
  
static class Face implements Cloneable{  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        return super.clone();  
    }  
}  

依此类推,如果Face对象还引用了其他的对象, 比如说Mouth,如果不经过处理,Body对象拷贝之后还是会通过一级一级的引用,引用到同一个Mouth对象。同理, 如果要让Body在引用链上完全独立, 只能显式的让Mouth对象也被拷贝。 到此,可以得到如下结论:如果在拷贝一个对象时,要想让这个拷贝的对象和源对象完全彼此独立,那么在引用链上的每一级对象都要被显式的拷贝。所以创建彻底的深拷贝是非常麻烦的,尤其是在引用关系非常复杂的情况下, 或者在引用链的某一级上引用了一个第三方的对象, 而这个对象没有实现clone方法, 那么在它之后的所有引用的对象都是被共享的。 举例来说,如果被Head引用的Face类是第三方库中的类,并且没有实现Cloneable接口,那么在Face之后的所有对象都会被拷贝前后的两个Body对象共同引用。假设Face对象内部组合了Mouth对象,并且Mouth对象内部组合了Tooth对象。

clone在平时项目的开发中可能用的不是很频繁,但是区分深拷贝和浅拷贝会让我们对java内存结构和运行方式有更深的了解。至于彻底深拷贝,几乎是不可能实现的。深拷贝和彻底深拷贝,在创建不可变对象时,可能对程序有着微妙的影响,可能会决定我们创建的不可变对象是不是真的不可变。clone的一个重要的应用也是用于不可变对象的创建。

4. 总结

(1)区别:如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实创建一个新的对象,则认为是浅拷贝;如果在对引用数据类型进行拷贝的时候,创建了新的对象,并且复制其内的成员变量,则认为是深拷贝;

(2)clone():是对当前对象进行浅拷贝,引用类型依然是在传递引用;

(3)如何进行深拷贝?

  • 序列化(serialization)这个对象,再反序列化回来,就可以得到这个新的对象,无非就是序列化的规则需要我们自己来写;

  • 继续利用并重写clone(),对拷贝对象内部的对象进行clone();

  • 引申:什么是序列化?

    序列化:将对象写入到IO流中;

    反序列化:从IO流中恢复对象;

    意义:序列化机制允许将实现序列化的Java对象转换为字节序列,这些字节序列可以保存再磁盘上,或者通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在;

    使用场景:所有可在网络上传输的对象都必须是可序列化的(引申:hdfs的RPC机制),比如RMI(remote method invoke,即远程方法调用)、RPC调用,传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的Java对象都必须的可序列化的。

四、Java中方法重载和重写的区别(多态的体现)

Java(面向对象编程)三大特性:封装、继承、多态

多态的体现:

(1)重载overloading:同一类中的同名函数,具有不同参数个数或类型(可以有不同的返回值和访问修饰符,但不能只有返回值和修饰符不同,参数列表必须是不同的),是一个类中多态性的体现。是由静态类型确定,再类加载的时候就确定,属于静态分派;

(2)重写overriding:子类中含有与父类相同名字、返回类型和参数表,构成重写,是再继承中多态性的体现,属于动态分派;

(3)方法的重载和重写都是实现多态的方式,区别在于:

  • 重载实现的是编译时的多态性,重写实现的是运行时的多态性;
  • 重载发生在一个类中,同名的方法如果由不同的参数列表则视为重载;
  • 重写发生在子类与父类之间,重写要求子类重写方法与父类被重写方法具有相同的参数列表,有兼容的返回类型(子类返回值类型不能大于父类),比父类被重写方法更好访问(被子类重写的方法不能拥有比父类方法更加严格的访问权限,父类方法是public,重写也必须是public;特殊情况:所以如果某一个方法在父类中的访问权限是private,那么就不能在子类中对其进行重写。如果重新定义,也只是定义了一个新的方法,不会达到重写的效果),不能比父类被重写方法声明更多的异常;
  • 重载对返回类型没有特殊要求,不能根据返回类型进行区分。

五、Java中“==”和“equals”的区别

首先分别回顾“==”和“equals”的使用:

1. ==运算符的使用

(1)可以使用在基本数据类型变量和引用数据类型变量中;

(2)如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等;

​ 如果比较的是引用数据类型变量:比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体;

补充:==符号使用时,必须保证符号左右两边变量类型一致(不一定类型要相同,因为有自动类型提升,但不代表任意两个类型都可以用“==”比较,两边的类型必须是可以统一的,比如boolean和其他基本数据类型就无法统一);

2. equals()方法的使用

(1)是一个方法,而非运算符;

(2)只能适用于引用数据类型;

(3)Object中equals()的定义(关键):

public boolean equals(Object obj) {
    return (this == obj);
}

Object类中定义的equals()和==的作用是相同的,比较两个对象的地址值是否相同,即两个引用是否只想同一个对象实体;

(4)像String、Date、File、包装类都重写了Object类中的equals()方法,重写以后,比较的不是引用地址,而是比较两个对象的“实体内容”是否相同;

(5)通常情况下,我们自定义的类如果使用equals()的话,也通常是比较两个对象的“实体内容”是否相同,那么我们九需要针对Object中的equals()进行重写;重写原则,比较两个对象实体内容是否相同。(关键)

3. “==”和“equals”的区别在于

(1)==既可以比较基本数据类型也可以比较引用类型,对于基本数据类型就是比较值,对于引用类型就是比较内存地址;

(2)equals是属于java.lang.Object类里的方法,如果该方法没有被重写过,默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中使用比较多,因此会形成equals是比较“值”的错误印象(也就是说equals的默认是“==”,但Object作为所有类的父类,为我们自定义的类提供了可以重写的方法,用来自定义对象相等的判断标准);

(3)具体要看自定义类里有没有重写Object的equals方法来判断;

(4)通常情况下,重写equals方法,会比较类中的相应属性是否都相等(这也是为什么Object中equals默认是“==”,因为类的各自的属性,无法统一)。

六、如何判断两个对象相等

参考资料

Java中如何判断两个对象是否相等(Java equals and ==)

1. ==和equals的区别

2. hashCode的作用及与equals的关系

新建一个类,尤其是业务相关的对象类的时候,最好复写equals方法,复写equals方法时,同时记着要复写hashcode方法,因为对于利用has进行存储对象的集合,比如HashMap、HashSet等,判断两个元素相等时会判断hashcode和equals都相等则认为相等;

(1)两个对象,如果a.equals(b)==true,那么a和b是否相等?

​ 相等,但地址不一定相等。

(2)两个对象,如果hashcode一样,那么两个对象是否相等?

​ 不一定相等,判断两个对象是否相等,需要判断equals是否为true。

对于两个对象,如果调用equals方法得到true,则两个对象的hashcode必定相等(原则上是这样要求的,但是实际上如果不重写hashcode方法,不一定是相等的);

如果equals返回false,hashcode也不一定不同;

如果hashcode不同,则equals结果必定为false;

如果hashcode相同,则equals结果不确定(看是否重写了equals,默认为==)。

七、Object类有哪些方法

  • clone():实现对象的浅拷贝,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常,我们有时候不希望在方法里将参数改变,就需要在类中重写clone方法;
  • getClass():final方法,返回运行时类型;
  • toString():返回对象类型和地址值,一般需要重写;
  • finalize():用于释放资源,很少显式使用,在垃圾回收时一定会被执行;
  • equals():默认==;
  • hashCode():减少在查找中使用equals的次数,重写equals方法时一般需要同时重写hashCode;
  • wait():(为什么操作线程的方法会在Object中?)wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait方法一直等待,直到获得锁或者被中断;
  • notify():唤醒在该对象上等待的某个线程;
  • notifyAll():唤醒在该对象上等待的所有线程。

八、Static关键字作用是什么

参考资料

Java中的static关键字解析

实例构造器是不是静态方法?

1. static关键字用途

“static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。”

​ ——《Java编程思想》

可以看出static关键字的基本作用,简而言之,一句话来描述就是:

方便在没有创建对象的情况下来进行调用(方法/变量)。

很显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

static可以用来修饰类的成员方法、类的成员变量,另外可以编写static代码块来优化程序性能。

(1)static方法

static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。

但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的

因此,如果说想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。我们最常见的static方法就是main方法,至于为什么main方法必须是static的,现在就很清楚了。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。

思考:构造器方法是否static?

“即使没有显示地使用static关键字,构造器实际上也是静态方法。“ ——《Java编程思想》

首先明确,构造器方法不是static的。

在Java中,“static”可以有多个意思,对方法而言,至少包括下面两点:

1、Java语言中的“static”关键字用于修饰方法时,表示“静态方法”,与“实例方法”相对。

2、在讨论方法的具体调用目标时,一个方法调用到底能否在运行前就确定一个固定的目标,是则可以进行“静态绑定”(static binding),否则需要做“运行时绑定”(runtime binding)。这与“static”关键字不是一回事。

首先看第一点:静态方法与实例方法两者的关键差异在于:“静态方法”的调用总是不指定某个对象实例为“接收者”,而“实例方法”则总是要以某个对象实例为“接收者”(receiver)。

在构造器中是可以访问“this”的;实例初始化器与实例变量初始化器在编译时会与构造器一起被收集到<init>()方法中,它们也都可以访问“this”。所以从Java语言的“static”关键字的角度看,实例构造器不是“静态方法”。

“类方法”(“静态方法”)与“实例方法”在概念中的JVM上的区别:在调用类方法时,所有参数按顺序存放于被调用方法的局部变量区中的连续区域,从局部变量0开始;在调用实例方法时,局部变量0用于存放传入的该方法所属的对象实例(Java语言中的“this”),所有参数从局部变量1开始存放在局部变量区的连续区域中。

从效果上看,这就等于在调用实例方法时总是把“this”作为第一个参数传入被调用方法。

第二点,Java语言中,虚方法(被重写的方法)可以通过覆写(override)的方式来实现子类型多态(subtype polymorphism)。Java语言支持三种多态,除了子类型多态外还有通过方法重载支持的ad-hoc多态(ad-hoc polymorphism)与通过泛型支持的参数化多态(parametric polymorphism)。在面向对象编程的语境里“多态”一般指子类型多态,下面提到“多态”一词也特定指子类型多态。

Java语言中非虚方法可以通过“静态绑定”(static binding)或者叫“早绑定”(early binding)来选择实际的调用目标——因为无法覆写,无法产生多态的效果,于是可能的调用目标总是固定的一个;虚方法则一般需要等到运行时根据“接收者”的具体类型来选择到底要调用哪个版本的方法,这个过程称为“运行时绑定”(runtime binding)或者叫“迟绑定”(late-binding)。

静态方法全部都是非虚的,而实例方法则看情况

行为如同“final”的方法都无法覆写,也就无法进行子类型多态;声明为final或private的方法都被属于这类。所以除了静态方法之外,声明为final或者private的实例方法也是非虚方法。其它实例方法都是虚方法。

“final方法”可以在运行时得到内联。其实所有非虚方法在运行时都可以安全的被内联。如果某个Java方法是final的或者不是虚方法的话,它就可以做静态绑定。

Java虚拟机规范第二版中定义了四种不同的字节码指令来处理Java程序中不同种类的方法的调用。包括,
· invokestatic - 用于调用类(静态)方法
· invokespecial - 用于调用实例方法,特化于super方法调用、private方法调用与构造器调用
· invokevirtual - 用于调用一般实例方法(包括声明为final但不为private的实例方法)
· invokeinterface - 用于调用接口方法

其中,invokestatic与invokespecial调用的目标必然是可以静态绑定的,因为它们都无法参与子类型多态;invokevirtual与invokeinterface的则一般需要做运行时绑定,JVM实现可以有选择的根据final或实际运行时类层次或类型反馈等信息试图进行静态绑定。

综上,那么Java中的实例构造器是不是“静态方法”呢?从Java语言规范中给出的“静态方法”的定义来看,答案是“否”——首先从Java语言规范对“方法”的定义来说,构造器根本不是“方法”;其次,实例构造器有一个隐式参数,“this”,在实例构造器中可以访问“this”,可以通过“this”访问到正在初始化的对象实例的所有实例成员。

A constructor is used in the creation of an object that is an instance of a class:

(… 省略)

Constructor declarations are not members. They are never inherited and therefore are not subject to hiding or overriding.

​ ——《Java Language Specification

实例构造器无法被隐藏或覆写,不参与多态,因而可以做静态绑定。从这个意义上可以认为实例构造器是“静态”的,但这种用法与Java语言定义的“静态方法”是两码事。

另外需要注意的是,Java语言中,实例构造器只能在new表达式(或别的构造器)中被调用,不能通过方法调用表达式来调用。new表达式作为一个整体保证了对象的创建与初始化是打包在一起进行的,不能分开进行;但实例构造器只负责对象初始化的部分,“创建对象”的部分是由new表达式本身保证的。

1、Java的实例构造器只负责初始化,不负责创建对象;Java虚拟机的字节码指令的设计也反映了这一点,有一个new指令专门用于创建对象实例,而调用实例构造器则使用invokespecial指令。
2、“this”是作为实例构造器的第一个实际参数传入的。

相信能区分“静态方法”与“静态绑定”中的“静态”之后,就不会再将Java中的实例构造器看作是“静态方法”了。

其他讨论:

  • 静态方法是不需要通过类的实例来调用的,而是类被加载后放在栈中的,是属于“类方法”。非静态方法是要通过类的实例才调用的,是属于 “对象方法”。从这个角度来讲,构造方法可以认为是静态的,因为在调用构造方法时必定是还未实例化的,而实例化后就不能再调用构造方法了。

  • 当时sun实现构造器就是为了给开发人员初始化变量的一种手段,可能其中某些特性与静态方法符合,比如无法覆盖,不参与多态等等,所以他就认为构造器是静态方法;其实严格来说,构造器存在于静态方法,实例方法之外的第三种方法,它会被虚拟机<init>静态方法调用。

(2)static变量

static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

static成员变量的初始化顺序按照定义的顺序进行初始化。

(3)static代码块

static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。

(4)static内部类

1、静态内部类中可以写哪些内容

1)匿名代码块

2)静态代码块

3)静态变量和非静态变量

4)静态方法和非静态方法

​ 注意:不能在静态内部类中写抽象方法

2、外部类如何调用静态内部类中的属性和方法

1)外部类可以通过创建静态内部类实例的方法来调用静态内部类的非静态属性和方法

2)外部类可以直接通过“ 外部类.内部类.属性(方法)” 的方式直接调用静态内部类中的静态属性和方法

3、静态内部类如何调用外部类的属性和方法

1)静态内部类可以直接调用外部类的静态属性和方法

2)静态内部类可以通过创建外部类实例的方法调用外部类的非静态属性和方法

4、如何创建静态内部类实例

1)在非外部类中:外部类名.内部类名 name = new 外部类名.内部类名();

2)在外部类中:内部类名 name = new 内部类名();

2. 易错点

(1)static关键字并不会改变变量和方法的访问权限;

(2)静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够);

(3)Java语法规定static是不允许用来修饰局部变量.

3. 总结

static关键字的作用是:

(1)修饰变量:因为类加载进方法去,所以多个对象是共享的(引申:JVM类加载);

(2)修饰方法:工具类方法,不需要建立对象,直接使用“类名.方法名”的方式调用;

(3)修饰代码块:只会在类被初次加载的时候执行一次,可用于初始化等操作;

(4)静态内部类;

注意:一般方法可以访问静态方法,但静态方法只能访问静态方法;

九、抽象类和接口的区别

参考资料

抽象类和接口的区别

JDK8新特性:接口的静态方法和默认方法

1. 抽象类(为了继承而存在)

(1)抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public;

(2)抽象类不能用来创建对象;

(3)如果一个类继承于抽象类,则子类必须实现父类的抽象方法,如果子类没有实现父类的抽象方法,则子类必须也定义为abstract类;

2. 接口(更加抽象)

(1)变量只能定义为public static final

(2)方法只能定义为public abstract

3. 区别

语法层面:

(1)抽象类可以提供成员方法的实现细节(有抽象方法的类一定是抽象类,抽象类中不一定有抽象方法),而接口中只能存在public abstract方法;

(2)抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型的;

(3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

注意:(3)中所述接口中不能含有静态代码块和静态方法是JDK8之前,JDK8新特性中允许接口中定义静态代码块和静态方法,还允许定义default方法,但都必须要有方法体,且静态方法不需要实现,default方法视情况而定:

在JDK8之前,下面的写法都是等价的:

public interface JDK8BeforeInterface {
    // 下两行变量定义等价
    public static final int field1 = 0;
    int field2 = 0;
    // 下两行方法声明等价
    public abstract void method1(int a) throws Exception;
    void method2(int a) throws Exception;
}

JDK8及以后,允许在接口中定义static方法和default方法:

public interface JDK8Interface {
 
    // static修饰符定义静态方法
    static void staticMethod() {
        System.out.println("接口中的静态方法");
    }
 
    // default修饰符定义默认方法
    default void defaultMethod() {
        System.out.println("接口中的默认方法");
    }
}

// 定义一个实现类
public class JDK8InterfaceImpl implements JDK8Interface {
    // 实现接口后,因为默认方法不是抽象方法,所以可以不重写,但是如果开发需要,也可以重写
    // 如果接口中的默认方法不能满足某个实现类需要,那么实现类可以覆盖默认方法
    
    // 签名跟接口default方法一致,但是不能再加default修饰符
    @Override
    public void defaultMethod() {
        System.out.println("接口实现类覆盖了接口中的default");
    }
}

静态方法,只能通过接口名调用,不可以通过实现类的类名或者实现类的对象调用;default方法,只能通过接口实现类的对象来调用:

public class Main {
    public static void main(String[] args) {
        // static方法必须通过接口类调用
        JDK8Interface.staticMethod();
 
        //default方法必须通过实现类的对象调用
        new JDK8InterfaceImpl().defaultMethod();
    }
}

由于java支持一个实现类可以实现多个接口,如果多个接口中存在同样的static和default方法会怎么样呢?

如果有两个接口中的静态方法一模一样,并且一个实现类同时实现了这两个接口,此时并不会产生错误,因为JDK8只能通过接口类调用接口中的静态方法,所以对编译器来说是可以区分的。但是如果两个接口中定义了一模一样的默认方法,并且一个实现类同时实现了这两个接口,那么必须在实现类中重写默认方法,否则编译失败

public interface JDK8Interface1 {
 
    // static修饰符定义静态方法
    static void staticMethod() {
        System.out.println("JDK8Interface1接口中的静态方法");
    }
 
    // default修饰符定义默认方法
    default void defaultMethod() {
        System.out.println("JDK8Interface1接口中的默认方法");
    }
}

public class JDK8InterfaceImpl implements JDK8Interface,JDK8Interface1 {
 
	// 由于JDK8Interface和JDK8Interface1中default方法一样,所以这里必须覆盖
    @Override
    public void defaultMethod() {
        System.out.println("接口实现类覆盖了接口中的default");
    }
}

public class Main {
    public static void main(String[] args) {
        JDK8Interface.staticMethod();
        JDK8Interface1.staticMethod();
        new JDK8InterfaceImpl().defaultMethod();
    }
}

img

(4)一个类只能继承一个抽象类,而一个类可以实现多个接口;

设计层面:

(5)类是“是不是”的关系,接口是“有没有”的关系,抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

(6)抽象类作为很多子类的父类,是一种模板式设计,而接口是一种行为规范,是一种辐射式设计。

什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

下面看一个网上流传最广泛的例子:门和警报的例子:门都有open( )和close( )两个动作,此时我们可以定义通过抽象类和接口来定义这个抽象概念:

abstract class Door {
    public abstract void open();
    public abstract void close();
}

interface Door {
    public abstract void open();
    public abstract void close();
}

但是现在如果我们需要门具有报警alarm( )的功能,那么该如何实现?下面提供两种思路:

1)将这三个功能都放在抽象类里面,但是这样一来所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能;

2)将这三个功能都放在接口里面,需要用到报警功能的类就需要实现这个接口中的open( )和close( ),也许这个类根本就不具备open( )和close( )这两个功能,比如火灾报警器。

从这里可以看出, Door的open() 、close()和alarm()根本就属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm()行为,Door设计为单独的一个抽象类,包含open和close两种行为。再设计一个报警门继承Door类和实现Alarm接口。

interface Alram {
    void alarm();
}
 
abstract class Door {
    void open();
    void close();
}
 
class AlarmDoor extends Door implements Alarm {
    void oepn() {
      //....
    }
    void close() {
      //....
    }
    void alarm() {
      //....
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值