Java面试题基础20题

大家好,我是蓝蓝。现在就开启 Java 的学习,终于开始 Java 的总结了,其中包含笔试篇,面试篇,笔试篇当然是针对笔试,面试按照章节来总结,比如今天总结时基本理论中常见的问题,后面还有集合,多线程,设计模式等等,我会尽快的保持质量的更,已经很快了。

一 概述相关

1 谈谈你对 Java 的理解

首先 Java 是一门面向对象的语言,其非常显著的特点有两个

  • 书写一次,到处运行

书写一次,到处运行,说明可以非常容易的获得跨平台能力。不过这样的回答些许不能让面试官满意,那就给他点颜色。其实,一次编译、到处运行说的是 Java 语言跨平台的特性,而 Java 的跨平台特性是和 Java 虚拟机有着密不可分的关系。

并不是说 Java 语言可以跨平台,而是在不同的平台都可以使用 Java 语言运行的环境罢了,更加严格的来说,跨平台的语言很多种,但是 Java 更加的成熟,

其实 Java 语言本身与其他的编程语言没有特别大的差异,并不是说 Java 语言可以跨平台,而是在不同的平台都有可以让 Java 语言运行的环境而已,所以一次编译,到处运行这种效果跟编译器有关。编程语言的处理需要编译器和解释器。

Java 虚拟机和 DOS 类似,相当于一个供程序运行的平台。

程序从源代码到运行的三个阶段:编码——编译——运行——调试。Java 在编译阶段则体现了跨平台的特点。编译过程大概是这样的:首先是将 Java 源代码转化成 .CLASS 文件字节码,这是第一次编译。 .CLASS 文件就是可以到处运行的文件。然后 Java 字节码会被转化为目标机器代码,这是是由 JVM 来执行的,即 Java 的第二次编译。“到处运行”的关键和前提就是 JVM。因为在第二次编译中 JVM 起着关键作用。在可以运行 Java 虚拟机的地方都内含着一个 JVM 操作系统。从而使 JAVA 提供了各种不同平台上的虚拟机制,因此实现了“到处运行”的效果。需要强调的一点是,Java 并不是编译机制,而是解释机制。Java 字节码的设计充分考虑了 JIT 这一即时编译方式,可以将字节码直接转化成高性能的本地机器码,这同样是虚拟机的一个构成部分。

  • 垃圾回收机制

Java 通过垃圾收集器回收分配内存,如果大家使用 C 写过代码就会知道,需要我们自行进行部分的内存管理,有时候忘记回收导致崩溃,在 Java 这里,开发人员不需要操心内存的分配和回收。

2. JDK 和 JRE 有什么区别?

JDK 是什么

JDK(Java Development Kit) Java 开发包或 Java 开发工具,是写 Java 应用程序的开发环境,其中包含了 Java 运行环境 (Java Runtime Enviroment) 和一些 Java核心库 (API)。

通过这个链接可以看到 Java 8中的这个图,点击这些链接你就可以看见 Java 几乎所有的概念,这也是最权威的描述。

https://docs.oracle.com/javase/8/docs/

JRE

JRE(Java Runtime Environment),翻译过来为 Java 的运行环境,其中包含了 Java 的虚拟机,Java 的基础类库。

综上两者,可以知道如果咱要使用 Java 语言编写 Java 程序,那么装好 JDK 就好了。JDK 包含了 JRE,同时还包含了编译 Java 源码的编译器 Javac,还包含了很多 Java 程序调试和分析的工具:jconsole,jvisualvm 等工具软件,还包含了 Java 程序编写所需的文档和 Demo 例子程序。、

如果你只是向运行 Java 程序,那装 JRE 就行。JRE 根据不同操作系统(如:WindowsLinux 等)和不同JRE提供商(IBM,ORACLE等)有很多版本,最常用的是 Oracle 公司收购 SUN 公司的 JRE 版本。如果你想查看更官方的解释,可以前往 Oracle 官网

3 Java和C++的区别

我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀!没办法!!!就算没学过C++,也要记下来!

  • 都是面向对象的语言,都支持封装、继承和多态
  • Java不提供指针来直接访问内存,程序内存更加安全
  • Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。
  • Java有自动内存管理机制,不需要程序员手动释放无用内存

二 语法相关

4 面向对象和面向过程的区别

面向对象

先来看枯燥无味的官方说法,把现实世界中的对象抽象地体现在编程世界中,一个对象代表了某个具体的操作。一个个对象最终组成了完整的程序设计,这些对象可以是独立存在的,也可以是从别的对象继承过来。对象之间通过相互作用传递信息,实现程序开发。

这就反复在听客家话。。。

翻译过来其实就是,我们都有手脚口鼻等一系列器官,这些器官就当作我们的属性,我们通过器官的喜怒哀乐就是我们的行为,当我们属性+行为组合就成为了对象。

好了,我是我,你是你,你可能比我高,比我帅,比我漂亮,即都是人类,人的总称,这就是相对对象的一种抽象。

类的具体表现或者实例就是对象,而对象的抽象或者总概括就是类。

再来看案例

public class Person {
    String name;
    int age;
    String gender;
    public Person() {
    }
    Person(String name,int age,String gender){
    this.name  = name;
    this.age = age;
    this.gender = gender;
    System.out.println(this.name+"对象被创建了"+",有"+this.age+"岁"+",是"+this.gender+"的");
    }
}

那么我们就来创建一个类

public static void main(String[] args) {
    Person p1 = new Person("蓝蓝", 18, "女");
    Person p2 = new Person("绿绿", 19, "男");    
}

下面的运行结果就是:

蓝蓝对象被创建了,有18岁,是女的
绿绿对象被创建了,有19岁,是男的

以上就是对面向对象的理解。面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。

面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。

面向过程

  • 基本概念

是一种非常早的编程思想,站在过程的角度思考问题,强调的是功能行为,功能的执行过程,过程执行的顺序,每个函数都自己一步步实现,使用的时候依次调用即可。

  • 面向过程的设计思想

最小单元为函数,每个函数自己实现,其中作为程序入口的函数称之为主函数,主函数依次调用其他函数,普通函数之间可以相互调用,从而实现整个系统功能。 面向过程最大的问题在于随着系统的膨胀,面向过程将无法应付,最终导致系统的崩溃。为了解决这一种软件危机,我们提出面向对象思想。

  • 面向过程的缺陷

是采用指定而下的设计模式,在设计阶段就需要考虑每一个模块应该分解成哪些子模块,每一个子模块又细分为更小的子模块,如此类推,直到将模块细化为一个个函数。

面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。

设计不够直观,与人类的思维习惯不一致 系统软件适应新差,可拓展性差,维护性低

5 面向对象三大特性

抽象

抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,就仿佛我关注你是否给我公众号文章点赞,转发,而我并不关注这些点赞转发行为的细节是什么。

封装

封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。

继承

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。

关于继承如下 3 点请记住:

  1. 子类拥有父类非 private 的属性和方法。
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。(以后介绍)。

举个例子

蓝蓝和绿贝都属于人类,他们都有姓名,性别,年龄,但是两者的爱好不一样。

从上图也可以发现,蓝蓝和绿绿有很多相通的属性和方法,这些方法和属性就可以完全提取出来放在一个父类中,这个父类让蓝蓝,绿贝继承。

继承关系

可以用概括的树形关系来表示,如下图所示。

类继承示例图

使用这种层次化的分类方式,是为了将多个类的通用属性和方法提取出来,放在它们的父类中,然后只需要在子类中各自定义自己独有的属性和方法,并以继承的形式在父类中获取它们的通用属性和方法即可。

继承是类与类的一种关系,是一种“is a”的关系。比如“狗”继承“动物”,这里动物类是狗类的父类或者基类,狗类是动物类的子类或者派生类。

Java 中的继承是单继承,即一个类只有一个父类。

Java中的继承只能单继承,但是可以通过内部类继承其他类来实现多继承。

public class Son extends Father{
    public void go () {
    System.out.println("son go");
}
public void eat () {
    System.out.println("son eat");
}
public void sleep() {
    System.out.println("zzzzzz");
}
public void cook() {
    //匿名内部类实现的多继承
    new Mother().cook();
    //内部类继承第二个父类来实现多继承
    Mom mom = new Mom();
    mom.cook();
}
private class Mom extends Mother {
    @Override
    public void cook() {
    System.out.println("mom cook");
            }
    }
}

什么是多态

所谓多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。

方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。

一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:

  • 方法重写(子类继承父类并重写父类中已有的或抽象的方法);
  • 对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

6 什么是多态机制?Java语言是如何实现多态的?

所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。

多态的实现

Java 实现多态有三个必要条件:继承、重写、向上转型。

继承:在多态中必须存在有继承关系的子类和父类。

重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。

对于 Java 而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。

7 抽象类和接口的对比

抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。

从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

相同点

  • 接口和抽象类都不能实例化
  • 都位于继承的顶端,用于被其他实现或继承
  • 都包含抽象方法,其子类都必须覆写这些抽象方法

不同点

参数 抽象类 接口
声明 抽象类使用abstract关键字声明 接口使用interface关键字声明
实现 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现
构造器 抽象类可以有构造器 接口不能有构造器
访问修饰符 抽象类中的方法可以是任意访问修饰符 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected
多继承 一个类最多只能继承一个抽象类 一个类可以实现多个接口
字段声明 抽象类的字段声明可以是任意的 接口的字段默认都是 static 和 final 的

备注:Java8 中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。

现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。

接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:

  • 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。

8 代码中如何实现多态

接口的意义

接口的意义用三个词就可以概括:规范,扩展,回调。

抽象类的意义

抽象类的意义可以用三句话来概括:

  1. 为其他子类提供一个公共的类型
  2. 封装子类中重复定义的内容
  3. 定义抽象方法,子类虽然有不同的实现,但是定义时一致的

9 接口和抽象类的区别

比较 抽象类 接口
默认方法 抽象类可以有默认的方法实现 java 8之前,接口中不存在方法的实现.
实现方式 子类使用extends关键字来继承抽象类.如果子类不是抽象类,子类需要提供抽象类中所声明方法的实现. 子类使用implements来实现接口,需要提供接口中所有声明的实现.
构造器 抽象类中可以有构造器, 接口中不能
和正常类区别 抽象类不能被实例化 接口则是完全不同的类型
访问修饰符 抽象方法可以有public,protected和default等修饰 接口默认是public,不能使用其他修饰符
多继承 一个子类只能存在一个父类 一个子类可以存在多个接口
添加新方法 想抽象类中添加新方法,可以提供默认的实现,因此可以不修改子类现有的代码

10 重载和重写的区别?

  • 重载发生在同一个类中方法名必须相同,实质表现就是多个具有不同的参数个数或者类型同名函数返回值类型可随意,不能以返回类型作为重载函数的区分标准),返回值类型、访问修饰符可以不同,发生在编译时。   
  • 重写发生在父子类中方法名、参数列表必须相同是父类与子类之间的多态性,实质是对父类的函数进行重新定义返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类如果父类方法访问修饰符为 private 则子类就不能重写该方法

问:Java 构造方法能否被重写和重载?

  重写是子类方法重写父类的方法,重写的方法名不变,而类的构造方法名必须与类名一致,假设父类的构造方法如果能够被子类重写则子类类名必须与父类类名一致才行,所以 Java 的构造方法是不能被重写的。而重载是针对同一个的,所以构造方法可以被重载

11 构造方法有哪些特性?

  1. 名字与类名相同。
  2. 没有返回值,但不能用void声明构造函数。
  3. 构造方法可以没有(默认一个无参构造方法),也可以有多个构造方法,他们之间构成重载关系
  4. 如果定义有参构造函数,则无参构造函数将自动屏蔽
  5. 构造方法不能手动调用,在创建实例的时候自动调用构造方法

12 成员变量与局部变量的区别有那些?

  1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 publicprivatestatic 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  2. 从变量在内存中的存储方式来看:如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

13 Java 中 final、finally、finalize 的区别?

(1) final 是一个修饰符,

  1. 如果一个类被声明为 final 则其不能再派生出新的子类,所以一个类不能既被声明为 abstract 又被声明为 final [所谓不可同时出现]的;
  2. 将变量或方法声明为 final 可以保证它们在使用中不被改变(对于对象变量来说其引用不可变,即不能再指向其他的对象,但是对象的值可变)。

注:

  1. 被声明为 final 的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改,被声明为 final 的方法也同样只能使用不能重载。
  2. 使用 final 关键字如果编译器能够在编译阶段确定某变量的值则编译器就会把该变量当做编译期常量来使用,如果需要在运行时确定(譬如方法调用)则编译器就不会优化相关代码;将类、方法、变量声明为 final 能够提高性能,这样 JVM 就有机会进行估计并进行优化;接口中的变量都是 public static final 的。

final关键字主要用在三个地方:变量、方法、类。

  1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
  2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
  3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。

(2)finally:用来在异常处理中,如果抛出一个异常,则相匹配的 catch 子句就会执行,然后控制就会进入 finally

  finally 是对 Java 异常处理模型的最佳补充。finally 结构使代码总会执行,而不管无异常发生。使用 finally 可以维护对象的内部状态,并可以清理非内存资源。特别是在关闭数据库连接这方面,如果程序员把数据库连接的 close() 方法放到 finally 中,就会大大降低程序出错的几率。

异常处理:

  • try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
  • catch 块:用于处理try捕获到的异常。
  • finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。

在以下4种特殊情况下,finally块不会被执行

  1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行
  2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行
  3. 程序所在的线程死亡。
  4. 关闭CPU。

(3)finalize():是一个方法,它是在对象被垃圾回收之前由Java虚拟机来调用的。

  finalize() 方法是 GC 运行机制的一部分,finalize() 方法是在 GC 清理它所从属的对象时被调用的,如果执行它的过程中抛出了无法捕获的异常,GC 将终止对改对象的清理,并且该异常会被忽略;直到下一次 GC 开始清理这个对象时,它的 finalize() 会被再次调用。

三 对象相关

10 java 创建对象的几种方式

采用new

使用 new 关键字创建对象的时候,需要使用构造器

public class Demo1 {
    public  static void  main(String[] args){
        Demo1 demo1 = new Demo1();
        demo1.test(2);
    }
    public Demo1(){

    }
    public void test(int value){
        System.out.println("第一种创建对象得方式="+value);
    }
}

通过反射

使用反射机制创建对象,用 Class 类或 Constructor 类的 newInstance() 方法。需要使用构造器。

当使用 Class 类里的 newInstance() 方法,调用的是无参构造方法。

当使用 java.lang.reflect.Constructor 类里的 newInstance 方法,调用的是有参构造方法。

public class Demo2 {
    public  static void  main(String[] args) throws Exception{
        Demo2 demo2 = Demo2.class.newInstance();
        System.out.println(demo2);
    }
    public Demo2(){
        System.out.println("初始化时直接调用");
    }
}

public class Demo3 {
    public  static void  main(String[] args) throws Exception{
       //通过new关键字
        Demo3 demo3 = new Demo3("蓝蓝");
        System.out.println(demo3);

        //通过反射
        Class p1 = Class.forName("Demo3");
        Constructor p2 = p1.getConstructor(String.class);
        Demo3 p3 = (Demo3)p2.newInstance("蓝蓝");
        System.out.println(p3);
    }
    public Demo3(String name){
        System.out.println("name"+name);
    }
}

采用 clone

需要实现 Cloneable 接口,重写 object 类的 clone 方法。无论何时我们调用一个对象的 clone 方法,JVM 就会创建一个新的对象,将前面对象的内容全部拷贝进去。用clone方法创建对象并不会调用任何构造函数。

通过序列化机制

通过 ObjectInputStreamreadObject() 方法反序列化类当我们序列化和反序列化一个对象,JVM 会给我们创建一个单独的对象。为了反序列化一个对象,我们需要让我们的类实现 Serializable 接口。在反序列化时,JVM 创建对象并不会调用任何构造函数。

11 java中==和eqauls()的区别

搞清楚这个问题之前,需要知道什么是基本数据类型,什么是引用数据类型,然后尽量的从底层的角度去回答可能会更加的清晰。

基本数据类型

也叫做原始数据类型,它们之间的比较实用 == ,比较的是它们之间的值。

引用数据类型

如果是引用数据类型,当使用==进行比较的时候,比较的是他们在内存中的地址。

当复合数据类型之间进行equals比较时,这个方法的初始行为是比较对象在堆内存中的地址。

在原始的 equals() 方法中,主要用来你判断其他对象是否和该对象相等,长成这个样子

//equals()方法在object类中定义如下: 
public boolean equals(Object obj) {  
    return (this == obj);  
}  

但是很多时候,在String,Data等类中把 Object 中的这个方法覆盖了,变为了比较值是否相同。

    // 比如在String类中如下:
    public boolean equals(Object var1) {
        if(this == var1) {
            return true;
        } else {
            if(var1 instanceof String) {
                String var2 = (String)var1;
                int var3 = this.value.length;
                if(var3 == var2.value.length) {
                    char[] var4 = this.value;
                    char[] var5 = var2.value;

                    for(int var6 = 0; var3-- != 0; ++var6) {
                        if(var4[var6] != var5[var6]) {
                            return false;
                        }
                    }

                    return true;
                }
            }

            return false;
        }
    }

从这里就可以清楚的发现,实际上在比较其值是否相等。

举个例子

String a = "lanlan";String b = "lanlan";System.out.println(a == b);//true

输出 true
说明:==在进行复合数据类型比较时,比较的是内存中的存放地址。因此a与b引用同一个String对象。

String b = "暖蓝笔记";String c = new String("暖蓝笔记");System.out.println(c == b);//falseSystem.out.println(c.equals(b));//true

输出:

false
true
说明:b,c分别引用了两个对象。显然,两者内容是相同的,因此 equal 返回 true。第一个例子也一样。

进一步分析

String str1= "lanlan";   String str2= new String("lanlan");   String str3= str2;   

从图中可以发现每个 String 对象的内容实际是保存到堆内存中的,而且堆中的内容是相等的,但是对于str1和str2来说所指向的地址堆内存地址是不等的,所以尽管内容是相等的,但是地址值是不相等的
“==” 是用来进行数值比较的,所以 str1 和 str2 比较不相等,因为 str2 和 str3 指向同一个内存地址所以str2 和 str3 是相等的。所以“==”是用来进行地址值比较的。

12 为什么Java中1000==1000为false而100==100为true?

Integer i1 = 100, i2 = 100;System.out.println(i1 == i2);//trueInteger i3 = 1000, i4 = 1000;System.out.println(i3 == i4);//fales

查看 Integer.java 类,会发现有一个内部私有类,IntegerCache.java,它缓存了从 -128 到 127 之间的所有的整数对象。

  private static class IntegerCache {        static final int low = -128;        static final int high;        static final Integer[] cache;        private IntegerCache() {        }        static {            int var0 = 127;            String var1 = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");            int var2;            if(var1 != null) {                try {                    var2 = Integer.parseInt(var1);                    var2 = Math.max(var2, 127);                    var0 = Math.min(var2, 21***18);                } catch (NumberFormatException var4) {                    ;                }            }            high = var0;            cache = new Integer[high - -128 + 1];            var2 = -128;            for(int var3 = 0; var3 < cache.length; ++var3) {                cache[var3] = new Integer(var2++);            }            assert high >= 127;        }    }

所以例子中i1和i2指向了一个对象。因此100==100为true。

13 equals()和hashcode()的联系

hashCode()是Object类的一个方法,返回一个哈希值。如果两个对象根据equal()方法比较相等,那么调用这两个对象中任意一个对象的hashCode()方法必须产生相同的哈希值。
如果两个对象根据eqaul()方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的。)

在笔试和面试,这个问题都是非常的常见,所以在这里会稍微详细的了解这个问题。

首先需要知道 equals 是 object 的方法,所以只能用于对象之间,基本类型使用 == ,反则封装类型用 equals。

public static void main(String[] args) {  Stu s1 = new Stu("张三", 18);  Stu s2 = new Stu("张三", 18);  System.out.println("stu:" + s1.equals(s2));  Integer i1 = new Integer(18);  Integer i2 = new Integer(18);  System.out.println("Integer:" + i1.equals(i2));  String str1 = "张三";  String str2 = "张三";  System.out.println("String:" + str1.equals(str2));}

上述结果为

stu:falseInteger:trueString:true

我们来查看这几个部分中equals的源码

stu

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

Integer

public boolean equals(Object obj) {    if (obj instanceof Integer) {        return value == ((Integer)obj).intValue();    }    return false;}

string

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;}

stu 中没有重写 equals的方法,直接使用了父类的object方法,而后面的 integer和String都各自实现了自己的 equals方法,所以Integer(基本类型)的equals实际上都是用的自己的实际值比较,String则是逐个char比较相等于否。

Hashcode什么用呢

先看看官方怎么说的

hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。

hashCode 的常规协定是:
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。

以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。

实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)

当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

翻译过来,即

  • HashCode不相等的两个对象equals一定不相等,但是hashCode相等的两个对象equals不一定相等;
  • 对象equals方法参与运算的自身属性attr不能被修改,并且同一个对象的hashCode值任何时候的返回值都应该相等;
  • 根据规定,重写对象的equals方法必须重写hashCode方法,尽管不写也能通过编译;

可以直接这样记忆还是不好理解,没关系,我们继续看例子。

假设内存位置1,2,3,4,5,6,7,8,如果不使用 hashcode 而任意的存放,那么当需要查找的时候,就需要到这 8 个位置挨个查找,如果使用 hashcode 就方便了很多。9 除 8 的余数为1,那么我们就把该类存在 1 这个位置,如果ID是 13,求得的余数是 5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过ID除 8 求余数直接找到存放的位置了。

如果两个类有相同的 hashcode 怎么办呢,比如如 9 除以 8 和 17 除以 8 的余数都是1,那么这是不是合法的,回答是:完全合法。那么如何判断呢?在这个时候就需要定义equals了。

所以,我们通过 hashcode 来判断两个类是否存在某个桶中,这个桶中可能会有很多类,我们再通过 equals 方法在桶中找到我们要的类。

理论差不多就这样,我们用代码的形式再验证这个结论。

public static void main(String[] args) {  Stu s1 = new Stu("张三", 18);  Stu s2 = new Stu("张三", 18);  System.out.println("stu:" + s1.equals(s2));  Set<Stu> set = new HashSet<>();  set.add(s1);  System.out.println("s1 hashCode:" + s1.hashCode());  System.out.println("add s1 size:" + set.size());  set.add(s2);  System.out.println("s2 hashCode:" + s2.hashCode());  System.out.println("add s2 size::" + set.size());}

输出结果为

stu:falses1 hashCode:13***55add s1 size:1s2 hashCode:46***62add s2 size::2

Java 的集合 set 是不允许有重复元素,所以这里的 size 从 1 变为了 2,由于 stu 中都是 new 出来的对象,分配地址不一样,那 set 是通过 equals 定义重复的吗

我们重写 stu 的equals 方法

@Overridepublic boolean equals(Object obj) {  if (obj == null){    return false;  }  if (obj.getClass() != getClass()){    return false;  }  return ((Stu)obj).getName().equals(getName());}

输出结果

stu:trues1 hashCode:71***46add s1 size:1s2 hashCode:11***27add s2 size::2

重写 equals 方法,name 相同就让 equals 返回 true 了,但是 Setsize 还是发生了改变,就说明不是有 equals 方法来定义重复的,现在仅仅重写 hashCode 方法:

@Overridepublic int hashCode() {  return getName().hashCode();}

输出结果

stu:falses1 hashCode:774889add s1 size:1s2 hashCode:774889add s2 size::2

仅重写了 hashCode 方法,所以 equals 返回 false,然后 hashCode 由 name 属性的 hashCode 方法得到,所以 hashCode 相等,但是 Set 的 size 还是改变了,这说明 Set 也不是仅仅依据 hashCode来定义重复。

那么现在将上述 equals 和 hashCode 两者同时重写,输出结果:

stu:trues1 hashCode:774889add s1 size:1s2 hashCode:774889add s2 size::1

结合上面引用的案例,可以类推,hash 类存储结构(HashSet、HashMap等等)添加元素会有重复性校验,校验的方式就是先取 hashCode 判断是否相等(找到对应的位置,该位置可能存在多个元素),然后再取equals方法比较(极大缩小比较范围,高效判断),最终判定该存储结构中是否有重复元素。

小结

  • hashCode 主要用于提升查询效率,来确定在散列结构中对象的存储地址;

  • 重写 equals() 必须重写 hashCode(),二者参与计算的自身属性字段应该相同;

  • hash 类型的存储结构,添加元素重复性校验的标准就是先取 hashCode 值,后判断equals();

  • equals() 相等的两个对象,hashcode() 一定相等;

  • 反过来:hashcode() 不等,一定能推出 equals() 也不等;

  • hashcode() 相等,equals() 可能相等,也可能不等。

14 a.hashCode()有什么用?与a.equals(b)有什么关系

hashCode() 方法是相应对象整型的 hash 值。它常用于基于 hash 的集合类,如 HashtableHashMapLinkedHashMap *等等。它与 *equals() 方法关系特别紧密。根据 Java 规范,使用 equal() 方法来判断两个相等的对象,必须具有相同的 hashcode

将对象放入到集合中时,首先判断要放入对象的 hashcode 是否已经在集合中存在,不存在则直接放入集合。如果 hashcode 相等,然后通过 equal() 方法判断要放入对象与集合中的任意对象是否相等:如果 equal() 判断不相等,直接将该元素放入集合中,否则不放入。

15 有没有可能两个不相等的对象有相同的hashcode

有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。如果两个对象相等,必须有相同的 hashcode 值,反之不成立。

16 a==b与a.equals(b)有什么区别

如果 ab 都是对象,则 a==b 是比较两个对象的引用,只有当 ab 指向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方法,所以可以用于两个不同对象,但是包含的字母相同的比较。

17 你知道 Object 中哪些方法

equals(),clone(),getClass(),notify(),notifyAll(),wait(),toString()

18 final有哪些用法?

final 也是很多面试喜欢问的地方,但我觉得这个问题很无聊,通常能回答下以下5点就不错了:

被 final 修饰的类不可以被继承
被 final 修饰的方法不可以被重写
被 final 修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
被 final 修饰的方法,JVM 会尝试将其内联,以提高运行效率
被 final 修饰的常量,在编译阶段会存入常量池中.
除此之外,编译器对 final 域要遵守的两个重排序规则更好:

在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
初次读一个包含 final 域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序.

#高频知识点汇总##Java##学习路径#
全部评论
https://github.com/MikeCreken/lanlanInterview 宝藏
1 回复
分享
发布于 2022-02-21 19:58
🎉恭喜牛友成功参与 【创作激励计划】高频知识点汇总专场,并通过审核! 前50位用户可直接获得牛可乐抱枕1个哦~ ------------------- 创作激励计划5大主题专场等你来写,最高可领取500元京东卡和500元实物奖品! 👉快来参加吧:https://www.nowcoder.com/discuss/804743
点赞 回复
分享
发布于 2021-11-26 12:26
阅文集团
校招火热招聘中
官网直投
好家伙 蓝蓝绿绿创建对象那里 我看成了蓝蓝被绿了😂😂
点赞 回复
分享
发布于 2021-11-28 20:10

相关推荐

点赞 评论 收藏
转发
6 58 评论
分享
牛客网
牛客企业服务