java基础及面试题(3.1----面向对象编程)

该文主要是对前面的   java基础及面试题(3.0---面向对象编程)文章进行比较细节的知识点的补充

目录

 

1、类方法(静态方法)和对象方法

2、构造方法

3、局部变量和全局变量

4、各个代码块的执行顺序

5、Abstract(抽象类)和interface(接口)

6、super和this

7、final       

8、内部类

9、值传递和引用传递

10、重写和重载

12、重写hashcode、equals、==(重点)

13、


1、类方法(静态方法)和对象方法

类方法

用static修饰的方法。(即静态方法)

1.类方法中不能引用对象变量;

2.类方法中不能调用类的对象方法;

3.在类方法中不能调使用super,this关键字;

4.类方法不能被覆盖。

实例方法

当一个类创建了一个对象后,这个对象就可以调用该类的方法(对象方法)(即非静态方法)。

1.实例方法中可以引用对象变量,也可以引用类变量;

2.实例方法中可以调用类方法;

3.对象方法中可以使用super,this关键字。

区别

      当类的字节码文件被加载到内存时,类的实例方法不会被分配入口地址,当该类创建对象后,类中的实例方法才分配入口地址,从而实例方法可以被类创建的任何对象调用执行。需要注意的是,当我们创建第一个对象时,类中的实例方法就分配了入口地址,当再创建对象时,不再分配入口地址,也就是说,方法的入口地址被所有的对象共享,当所有的对象都不存在时,方法的入口地址才被取消。(非静态方法)

     对于类中的类方法,在该类被加载到内存时,就分配了相应的入口地址。从而类方法不仅可以被类创建的任何对象调用执行,也可以直接通过类名调用。类方法的入口地址直到程序退出时才被取消。(静态方法)

    类方法在类的字节码加载到内存时就分配了入口地址。因此,Java语言允许通过类名直接调用类方法,而实例方法不能通过类名调用。在Java语言中,类中的类方法不可以操作实例变量,也不可以调用实例方法,这是因为在类创建对象之前,实例成员变量还没有分配内存,而且实例方法也没有入口地址。

2、构造方法

1、如无构造函数则默认生成一个无参构造函数,构造函数没有返回值类型,如有返回值类型(void或其他)则为普通方法而不是构造函数

3、局部变量和全局变量

局部变量和全局变量

public class Test{

static{    //静态代码块

   int x=5; //局部变量是要赋值的,不然会编译报错(全局变量是编译时就存进去的,系统会给对应的默认值,而局部变量是运行到时才进入栈中,系统没有给对应的默认值,所以局部变量要先赋值)

}

static int x,y;    //静态属性

public static void main(String args[]){     //main方法

   x--;

   myMethod( );

   System.out.println(x+y+ ++x);

}

public static void myMethod( ){            //静态方法,即类方法

  y=x++ + ++x;

 }

}

1.静态语句块中x为局部变量,不影响静态变量x的值(局部变量不影响全局变量,局部变量是运行时加载,全局变量是编译时就加载了)(静态属性/方法/代码块都是最先加载到方法区的资源)

2.xy为静态变量,默认初始值为0,属于当前类,其值得改变会影响整个类运行。

3.java中自增操作非原子性的 (原子性简单点讲就是在运算时是不受其他线程影响的,是独立安全运行完的。非原子性则是在++的过程还没有运算完就被其他的线程干扰改变了其中的值,导致最后运算结果不对。)

main方法中:

  • 执行x-- x=-1
  • 调用myMethod方法,x执行x++结果为-1(++),但x=0++x结果1x=1 ,则y=0
  • x+y+ ++x,先执行x+y,结果为1,执行++x结果为2,得到最终结果为3
  •  
方法调用时,会创建栈帧在栈中,调用完是程序自动出栈释放,而不是gc释放,垃圾回收器是主要是针对堆区。

4、各个代码块的执行顺序

静态代码块,普通代码块,构造函数的初始化顺序:

Java类的初始化顺序:1. 静态代码块;2. 普通代码块;3. 构造函数。

其次,静态代码块只加载一次;普通代码块创建几个对象就加载几次。

那么问题来了普通代码块是方法?方法代码不是说只创建一次吗?

------普通代码块即构造代码块(一般用来初始化)不是方法

         静态代码块:用staitc声明,jvm加载类时执行,仅执行一次
         构造代码块:类中直接用{}定义,每一次创建对象时执行。(即普通代码块,方法是只执行一次)
         执行顺序优先级:静态块,main(),构造块,构造方法。

关于执行顺序也有子类的情况时的执行顺序:

父类--静态变量
父类--静态初始化块
子类--静态变量
子类--静态初始化块
子类main方法
父类--变量
父类--初始化块
父类--构造器
子类--变量
子类--初始化块
子类--构造器         (静态最先---main---非静态(变量--初始化块--构造器),父类先于子类)

构造函数在一定意义上是静态的,静态初始化块在类加载时完成,无法人为调用。 静态初始化块的标准写法,没有访问修饰符、参数:静态初始化块  static {// 初始化内容 }

5、Abstract(抽象类)和interface(接口)

         public abstract class Test {

       abstract void method() {

}}

Abstract不能有实现体(即不能有{}

1抽象类不能被实例化,实例化的工作应该交由它的子类来完成,它只需要有一个引用即可。

2抽象方法必须由子类来进行重写。

3只要包含一个抽象方法的类,该类必须要定义成抽象类,不管是否还包含有其他方法。

4抽象类中可以包含具体的方法,当然也可以不包含抽象方法。

5abstract不能与final并列修饰同一个类。

6abstract 不能与privatestaticfinalnative并列修饰同一个方法。、

insterface

接口的方法必须是public,interface中的方法默认为public abstract 的 ,变量默认为public static final(但其前的abstract可以省略),所以抽象类中的抽象方法不能用的访问修饰符这里也不能用。

jdk1.8中:接口可以有实现方法(该方法必须是static/default),由接口调用。

 

抽象类和接口的区别:

1.抽象类可以有构造方法,接口中不能有构造方法。

2.抽象类中可以有普通成员变量,接口中没有普通成员变量

3.抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。

4、抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。

关于抽象类

JDK 1.8以前,抽象类的方法默认访问权限为protected

JDK 1.8时,抽象类的方法默认访问权限变为default

关于接口

JDK 1.8以前,接口中的方法必须是public

JDK 1.8时,接口中的方法可以是public的,也可以是default

JDK 1.9时,接口中的方法可以是private

虽然说在Java8可以在接口中定义静态方法了(此处定义指的是含body的实现),但是不能只声明,只声明的会被编译器识别为抽象方法,而抽象方法不能用static修饰 

接口中的属性在不提供修饰符修饰的情况下,会自动加上public static final

注意(在1.8的编译器下可试):

1)属性不能用privateprotected,default 修饰,因为默认是public

2)如果属性是基本数据类型,需要赋初始值,若是引用类型,也需要初始化,因为默认有final修饰,必须赋初始值;(接口的变量/引用等都要初始化,因为默认是final

3)接口中常规的来说不能够定义方法体,所以无法通过getset方法获取属性值,所以属性不属于对象,属于类(接口),因为默认使用static修饰。default不能修饰变量
接口中只有常量定义,没有变量声明。即接口的变量只能是public final static修饰所以接口中只有常量。

总结:抽象类:可有构造方法,子类继承不想覆写所有抽象方法可以命名为抽象子类,可以有普通成员变量,可以有普通方法                      (即实现了的方法),可以有任意访问类型的静态变量

           接口:不能有构造方法,子类实现必须全部覆写,不能有普通成员变量,1.8后允许有static/default的方法(之前不允                       许有实现体),静态成员变量必须public static final(可省略写,默认是,即全局变量)

        接口不能用private、static、synchronized、native,protected访问修饰符修饰(接口的普通方法/接口只能是public 和abstract修饰其他都不行)(抽象类可以有部分实现,但抽象方法不能被private、static、synchronized、native,final等修饰,abstract不能与final并列修饰同一个类。)(两者都不能实例化)

6、super和this

 1)调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。

2super()this()类似,区别是,super从子类中调用父类的构造方法,this()在同一类内调用其它方法。

3super()this()均需放在构造方法内第一行。

4)尽管可以用this调用一个构造器,但却不能调用两个。

5this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过

6this()super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。

7)从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

 

总结:每次实例化一个父类对象,因为是子类实例化父类对象,所有每次子类构造器都会先隐形的去调用父类对应的构造器,所以不能super和this同时存在,会重复调用,则会编译出错。(从这些可以认识到:当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;执行父类构造器时,系统会再次上溯执行其父类构造器… …依此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器。)

 

this()super()为构造方法,作用是在JVM堆中构建出一个对象。因此避免多次创建对象,同一个方法内只能调用一次this()super()。同时为了避免操作对象时对象还未构建成功,需要this()super()的调用在第一行实现【以此来创建对象】,防止异常。

 子类的构造方法总是先调用父类的构造方法,如果子类的构造方法没有明显地指明使用父类的哪个构造方法,子类就调用父类不带参数的构造方法。

而父类没有无参的构造函数,所以子类需要在自己的构造函数中显示的调用父类的构造函数。

子类继承父类,继承了哪些东西?

1.继承public和protected修饰的属性和方法,不管子类和父类是否在同一个包;

2.继承默认权限修饰符修饰的属性和方法,前提是子类和父类在同一个包。

(也有一种说法:子类能继承父类的所有成员(除构造函数),只是一些成员不能调用访问。例private)

7、final       

 1)、 final修饰类:类不能继承     final修饰方法:方法不能重写     final修饰变量:变量不能值不能变/引用不能变

         注意final类中的所有成员方法都会被隐式地指定为final方法。

2)、final类型的变量一定要初始化,因为final的变量不可更改。为什么?

final修饰的变量表示赋值之后不能再进行更改,系统赋默认值也算赋值,如果给其赋予了默认值程序员则不能赋予自己要赋予的值,因此系统也不会赋给final变量等的默认值。所以final必须在使用前赋值。

        final变量的初始化只有三种方式: 声明时初始化 ,或者构造函数中初始化,或者在代码块中初始化,不能在普通方法中对final进行使用前的初始化

3)、对于形式参数只能用final修饰符,其它任何修饰符都会引起编译器错误 。但是用这个修饰符也有一定的限制,就是在方法中不能对参数做任何修改。  不过一般情况下,一个方法的形参不用final修饰。只有在特殊情况下,那就是:方法内部类。   一个方法内的内部类如果使用了这个方法的参数或者局部变量的话,这个参数或局部变量应该是final 

对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改(类似常量);如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

8、内部类

一个.java文件中,可以有多个类,包括内部类和外部类。考虑到内部类的原因,一个.java文件可以中可以有多个public类。

但是对于外部类而言,一个.java文件必须只能有一个public类,同时这个类的类名必须和.java的文件名一致(包括大小写)。

public类不是必须的,但是如果源文件中有一个(只能有一个)public类的话,文件名必须与这个public类同名,原因 是为了方便虚拟机在相应的路径中找到相应的类所对应的字节码文件。所以在没有public类的Java文件中,文件名和类名都没什么联系。

结论:

一个Java源文件中最多只能有一个public类,当有一个public类时,源文件名必

须与之一致,否则无法编译,如果源文件中没有一个public类,则文件名与类中没有一致性要求。
至于main()不是必须要放在public类中才能运行程序。

 

匿名内部类的创建格式为: new 父类构造器(参数列表)|实现接口(){

                                             //匿名内部类的类体实现

                                        }

1使用匿名内部类时,必须继承一个类或实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。

2匿名内部类由于没有名字,因此不能定义构造函数

3匿名内部类中不能含有静态成员变量和静态方法

4匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。

5匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。

(外部类只能public/default)

1、外部类前可以修饰:publicdefaultabstractfinal

2、内部类前可以修饰:publicprotecteddefaultprivateabstractfinalstatic

3、局部内部类前可以修饰:abstractfinal

 

其中:访问修饰符(publicprotecteddefaultprivate),其他都是非访问修饰符

在Java中,可以将一个类定义在另一个类里面或者一个方法里边,这样的类称为内部类,广泛意义上的内部类一般包括四种:成员内部类,局部内部类,匿名内部类,静态内部类 。

1.成员内部类

(1)该类像是外部类的一个成员,可以无条件的访问外部类的所有成员属性和成员方法(包括private成员和静态成员);

(2)成员内部类拥有与外部类同名的成员变量时,会发生隐藏现象,即默认情况下访问的是成员内部类中的成员。如果要访问外部类中的成员,需要以下形式访问:【外部类.this.成员变量  或  外部类.this.成员方法】;

(3)在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问;

(4)成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象;

(5)内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。如果成员内部类用private修饰,则只能在外部类的内部访问;如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。外部类只能被public和包访问两种权限修饰。

2.局部内部类

(1)局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内;

(2)局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

3.匿名内部类

(1)一般使用匿名内部类的方法来编写事件监听代码;

(2)匿名内部类是不能有访问修饰符和static修饰符的;

(3)匿名内部类是唯一一种没有构造器的类;

(4)匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

4.内部静态类

(1)静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似;

(2)不能使用外部类的非static成员变量或者方法。

静态变量只能定义在类的内部,不可以定义在静态块或方法中
可以在类内部定义静态变量,在静态块中进行初始化操作,因为类的内部是不允许有操作语句存在的,比如JDBC操作,所以可以在静态块static{} 中进行初始化操作,如:JDBC
定义静态变量主要是为了供外部访问,定义在一个局部中外部没有权限访问,为什么要定义呢,而且不能定义

定义了会报错,而且static属于类,在方法中定义了没有意义

请说明内部类可以引用他包含类的成员吗,如果可以,有没有什么限制吗?

参考回答:

一个内部类对象可以访问创建它的外部类对象的内容,

内部类如果不是static的,那么它可以访问创建它的外部类对象的所有属性内部类(有静态属性/方法一定是静态类)

如果是sattic的,即为nested class,那么它只可以访问创建它的外部类对象的所有static属性一般普通类只有public或package的访问修饰,

而内部类可以实现static,protected,private等访问修饰。当从外部类继承的时候,内部类是不会被覆盖的,它们是完全独立的实体,每个都在自己的命名空间内,如果从内部类中明确地继承,就可以覆盖原来内部类的方法。

外部类表示当前对象:外部类.this  外部类创建内部类对象:static内部类—内部类 对象 = new 内部类();非static---内部类 对象 = new 外部类().内部类();

外部类和内部类互相访问总结:外部类访问内部类:必须建立内部类对象   ;内部类可以直接访问外部类的属性/方法,因为外部类持有内部类的引用(static则只能访问外部类的static的)

(包括访问私有属性)(内部类访问外部类的属性一般都会用final,不写默认会加上。如不用fianl修饰如内部类引用了,但当外部类用完该变量后,可能被gc回收了使得内部类无法使用该变量,定义为final则会复制一份作为成员变量放于内部类中,而final所指对象地址不变,防止内部类改变其引用导致引用不一致。)

9、值传递和引用传递

 

在方法中,改变一个对象参数的引用不会影响到原始引用。这是很自然的。

举个例子:
假设在函数外有 A a = new A();那么引用a指向堆中的一个对象A()。
假设有一个函数:
void f(A a){  
a = new A(); 
}

显然,这里a指向了堆中的另一个对象A(),而在函数外的那个引用a依然没有改变,指向原来的对象A()。

class Two{

    Byte x;  //这里是包装类

}

class PassO{

    public static void main(String[] args){

        PassO p=new PassO();  

        p.start();   

    }

    void start(){

        Two t=new Two();     //创建一个Two类对象

        System.out.print(t.x+””);   //全局变量byte默认0,但是由于Two类的属性是一个包装类的引用类型,所以是null

        Two t2=fix(t);              //此时调用fix方法返回Two对象,此时将前面的t对象传入,t传入的是地址,fix的方法只能改变                                              //该地址所指向的值/地址,不能改变t对象的地址,只能改变t对象地址所指向的值.

        System.out.print(t.x+” ” +t2.x);   //所以此时是42   42    (t是原对象,而t2是fix方法返回的对象)

    }

    Two fix(Two tt){

        tt.x=42;

        return tt;

    }

}

注意xByte类型,也就是byte的包装类型,属于引用类型。实例该类对象时,如果成员变量没有显示初始化那么Java默认初始化为null.

该题中引用类型t作为形参进行传递,形参的改变会改变实参的值,所以再次打印t.x时已经变为42了。

https://uploadfiles.nowcoder.com/images/20160815/6316247_1471272120187_BFDB62242C2B290778BC75D881FF07FD

Java是值传递,即形参不改变实参;该题以引用作为实参传给形参,引用没改变(符合值传递)但引用指向的内容可以改变。(没改变引用地址,但改变了内容)

1)、为什么说java是值传递:

1、当传递的是基本数据类型,此时就是明显的值传递。

2、当传递的是引用类型时,其实传递的是引用类型对象的地址,由于该地址是对象一直持有的不会被改变,传过去改变的只是地址所指向的值(地址指向的值可以是数值/引用类型)

Float f1 == f3,f3没有new创建,而是由f1赋值,所以f3地址是指向f1的,f1值改变,相当于改变f3值,所以相等(f1==f3

 

2)、请你解释什么是值传递和引用传递?

值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量.

引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。 所以对引用对象进行操作会同时改变原对象.

一般认为,java内的传递都是值传递.

3)、Java为什么只有值传递??

        值传递(pass by value)是指在调用函数时将实际参数“复制”一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

        引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

基本类型传递举例:String类型:

对象类型举例:传个对象给形参,在被调用的方法中改变了对象的内容。但仍然是值传递,该传递的是对象的引用即复制了一份对象的地址给形参(不是直接给),传递的是对象的引用而不是对象的内容,所以并没有影响到传递的参数即为值传递。(传递的值是对象的引用)(传递对象过来,改变其属性内容:setName=“100“ ,相当于把name的地址指向了另一个地址new String(”100“),而原来形参传递的对象地址没有发生改变。   方法中并不能对实参的地址做改变,形参传递的是对象引用(对象地址),也是复制地址副本传递过来的所以还是值传递。

 

10、重写和重载

方法重写(参数,返回类型都一样,访问权限比父类高)(两同两小一大原则,方法名相同,参数类型相同,子类返回类型小于等于父类方法返回类型, 子类抛出异常小于等于父类方法抛出异常, 子类访问权限大于等于父类方法访问权限。[注意:这里的返回类型必须要在有继承关系的前提下比较]

  • 参数列表必须完全与被重写方法的相同;
  • 返回类型必须完全与被重写方法的返回类型相同;
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected
  • 父类的成员方法只能被它的子类重写。
  • 声明为final的方法不能被重写。
  • 声明为static的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为privatefinal的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为publicprotected的非final方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个方法,则不能重写这个方法。

方法重载(参数不同才是重载函数的区分标准(类型顺序个数不同),返回类型不是)

  • 被重载的方法必须改变参数列表(参数个数或类型或顺序不一样)
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。

11、继承和实现

Implements和extends使用注意:

1Java 中单实现通过 implements 关键字,多实现通过 extends 关键字

2Java 中单继承通过 extends 关键字,没有多继承

3、如果同时出现继承和实现,则必须先继承(extends)再实现(implements

子类不继承父类构造函数,继承其他所有的成员,只是一些成员子类不能访问例private修饰的

12、重写hashcode、equals、==(重点)

 请你说明符号“==”比较的是什么?(与equals的区别)  请你解释Object若不重写hashCode()的话,hashCode()如何计算出来的?

参考回答:

Object的hashcode方法----默认是对象的内存地址经哈希算法得到 。(两个对象地址不同则散列值                                  不同(除非数据非常大,即hash冲突))

覆写后的hashcode----将对象的属性也加入到散列值的计算中去,遵循两个equals为T的对象散列值相同的规则

Object的equals方法-----==  /instanceof String,是对对象引用地址的判断

覆写后的equals方法------如果==则直接返回T,否则比较各个元素是否相等是则返回T。

 

说到为什么重写equals时一定要重写hashcode?

        此时先从重写equals的目的开始说,重写equals的目的是为了判断两个对象是否等价(即内容是否相等)而非为了证明两个对象的唯一性(即==),这样我们在实现自己的类时就要去重写equals方法,而又为什么重写equals方法一定要去重写hashcode方法呢?这是为了遵循两个对象equals为T,则两对象的hashcode要相等的规则。而原生的hashcode是根据内存地址计算的散列值,此时存在两个equals为T的对象hashcode不等的现象。

        说完要遵循两个对象equals为T,则两对象的hashcode要相等的规则,那么又要提问:为什么要用这个规则?这个规则有什么用?

        这个问题应该是有个前提,就是你需要用到HashMap,HashSet等Java集合。用不到哈希表的话,其实仅仅重写equals()方法也可以吧。而工作中的场景是常常用到Java集合,所以Java官方建议 重写equals()就一定要重写hashCode()方法。而为什么用到hashmap、hashset等集合要遵守这个规则,此处以hashset的add调用了hashmap的put方法源码为例(该方法有判断两个对象是否相等的逻辑代码,即防止重复值添加的原理)

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        //这里通过哈希值定位到对象的大概存储位置
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //if语句中,先比较hashcode,再调用equals()比较
            //由于“&&”具有短路的功能,只要hashcode不同,也无需再调用equals方法
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

从代码中可以看到比较两个对象是否相等用了if (e.hash == hash && ((k = e.key) == key || key.equals(k)))来判断的,

该代码即先对对象的hash值进行比较,如果则不会执行equals方法(hashcode方法比equals方法效率高)此时则会提高代码效率。而如果这是自定义的类,此时重写了equals没重写hashcode,则此时会出现两个内容相同的对象由hashcode

不同而存在该集合中(即不同的hashcode对应着相同一个值,即出现了hashset中重复的现象了,hashset的value是不允许重复的)。所以说equals和hashcode的重写和集合的使用(也可以是自己自定义的类)是相关的,当然你也可以用equal而不用hashcode去直接判断两个对象,但此时的效率是比较低的,和重写hashcode的代价不成正比。(这里调用的是重写的hashcode,如没重写则调用原生的)

重写equals代码:equals(o){if(this==o) true       Else if(o==null || o.getclass()!=getclass())f;       Else equals/== 各个元素 ,全相同则t}

重写hashcode:加入对象中有prime、age属性

public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

(总结:只重写equals时当两对象==时据规则hashcode一定相等,没重写的hashcode产生的两对象hashcode不一定相同,从而前后矛盾,则也需要对hashcode进行重写)

1、hash冲突(即两个不同地址对象的hash值相同),由于hash具有优越的查询性能所以常用,但hash

是存在hash冲突的问题的,那怎么解决hash冲突?后面在讲到hashmap的文章会做解析

注意:对于重写hashcode方法也存在一定的问题:

(1)返回的hash值是int型的,防止溢出。

(2)不同的对象返回的hash值应该尽量不同。(为了hashMap等集合的效率问题)

(3)《Java编程思想》中提到一种情况

“设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。如果在讲一个对象用put()添加进HashMap时产生一个hashCdoe值,而用get()取出时却产生了另一个hashCode值,那么就无法获取该对象了。所以如果你的hashCode方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()方法就会生成一个不同的散列码”。

举个例子

public class Test {
    
    private int num;
    private String data;
    
    public Test(int num,String data){
        this.num = num;
        this.data = data;
    }
    
    public void setNum(int num) {
        this.num = num;
    }

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

        if ((obj == null) || (obj.getClass() != this.getClass()))
            return false;

        Test test = (Test) obj;
        return num == test.num&& (data == test.data || (data != null && data.equals(test.data)));
    }

    public int hashCode()
    {
        int hash = 7;
        hash = 31*hash+num;
        hash = 31*hash+data.hashCode();
        return hash;
        
    }
    
    public static void main(String[] args) {
        
        Map<Test,Integer> map = new HashMap<>();
        Test t1 = new Test(21,"ouym");
        map.put(t1, 1);
        t1.setNum(20);
        System.out.println(map.get(t1));
        
    }

}

输出值为null,我的天呐,hashMap取不到值了。

不重写equals和hashCode方法的话是不依赖于对象属性的变化的,也就是说这里使用默认的hashCode方法可以取到值。但是我们重写equal方法的初衷是判定name和num属性都相等的Test对象是相等的,而不是说同一个对象的引用才相等,而num=21和num=20明显不想等,所以这里hashCode返回值不同并不违背设计的初衷。注意上面代码的使用陷阱。

上面重写的hashcode可能会延伸到为什么是31的问题?

具体参考https://www.cnblogs.com/yuxiaole/p/9570850.html

13、

请说明”static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?

参考回答:

“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。 
Java
static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。

区分:程序在JVM运行过程中,会把类的类型信息、static属性和方法、final常量等元数据加载到方法区,这些在类被加载时就已经知道,不需对象的创建就能访问的,就是静态绑定的内容;需要等对象创建出来,使用时根据堆中的实例对象的类型才进行取用的就是动态绑定的内容。

静态绑定:

静态绑定(前期绑定 编译时)是指:在程序运行前就已经知道方法是属于那个类的,在编译的时候就可以连接到类的中,定位到这个方法。

Java中,finalprivatestatic修饰的方法以及构造函数都是静态绑定的,不需程序运行,不需具体的实例对象就可以知道这个方法的具体内容。

动态绑定:

动态绑定(后期绑定 运行时)是指:在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。

静态绑定不能被覆盖

该文部分内容参考https://blog.csdn.net/sixingmiyi39473/article/details/78306296

                           https://www.cnblogs.com/ouym/p/8963219.html

                            https://www.cnblogs.com/yuxiaole/p/9570850.html

发布了8 篇原创文章 · 获赞 1 · 访问量 159

猜你喜欢

转载自blog.csdn.net/qq_35599414/article/details/104981273
今日推荐