Java面试基础篇

以下内容来源自Java 面试指南 | JavaGuide(Java面试 + 学习指南)和自己的学习笔记整理,这里我整理了自己比较感兴趣的点,也有助于我自己理解~

变量

成员变量与局部变量的区别?

Java面试基础篇_第1张图片

成员变量 vs 局部变量

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

著作权归Guide所有 原文链接:Java基础常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)

我发现Guide哥的分类方式很好,简单的将变量分为成员变量和局部变量,其中成员变量包含类变量和实例变量。

静态变量有什么作用?

静态变量也就是被 static 关键字修饰的变量。==它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。==也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。

静态变量是通过类名来访问的,例如StaticVariableExample.staticVar(如果被 private关键字修饰就无法这样访问了)。


著作权归Guide所有 原文链接:Java基础常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)

主要就是静态变量只会被分配一次内存,可以被类的所有实例共享。

方法

重载和重写有什么区别?

总结

综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。

区别点 重载方法 重写方法
发生范围 同一个类 子类
参数列表 必须修改 一定不能修改
返回类型 可修改 子类方法返回值类型应比父类方法返回值类型更小或相等
异常 可修改 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
访问修饰符 可修改 一定不能做更严格的限制(可以降低限制)
发生阶段 编译期 运行期

方法的重写要遵循“两同两小一大”(以下内容摘录自《疯狂 Java 讲义》,issue#892open in new window ):

  • “两同”即方法名相同、形参列表相同;
  • “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
  • “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

⭐️ 关于 重写的返回值类型 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。

public class Hero {
    public String name() {
        return "超级英雄";
    }
}
public class SuperMan extends Hero{
    @Override
    public String name() {
        return "超人";
    }
    public Hero hero() {
        return new Hero();
    }
}

public class SuperSuperMan extends SuperMan {
    public String name() {
        return "超级超级英雄";
    }

    @Override
    public SuperMan hero() {
        return new SuperMan();
    }
}

著作权归Guide所有 原文链接:Java基础常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)

也就是说,重载是同样的一个方法,根据输入数据(参数)的不同,做出不同的处理,返回不同的结果~

重写则是当子类继承父类的相同方法时,输入的数据是一样,但是要做出和父类方法不同的响应,不同的处理,在这一点上,子类重写方法相当于覆盖了父类的方法。我认为这是重写和重载最大的区别。

静态方法为什么不能调用非静态成员?

这个需要结合 JVM 的相关知识,主要原因如下:

  1. 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
  2. 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
public class Example {
    // 定义一个字符型常量
    public static final char LETTER_A = 'A';

    // 定义一个字符串常量
    public static final String GREETING_MESSAGE = "Hello, world!";

    public static void main(String[] args) {
        // 输出字符型常量的值
        System.out.println("字符型常量的值为:" + LETTER_A);

        // 输出字符串常量的值
        System.out.println("字符串常量的值为:" + GREETING_MESSAGE);
    }
}

著作权归Guide所有 原文链接:Java基础常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)

也就是说,重点在于静态方法和类一起出现,你那边非静态成员和对象(实例)一起出现,静态方法在的时候,非静态成员还不存在呢,怎么调用?调用不了一点~

静态方法和实例方法有何不同?

1、调用方式

在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象

不过,需要注意的是一般不建议使用 对象.方法名 的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。

因此,一般建议使用 类名.方法名 的方式来调用静态方法。

public class Person {
 public void method() {
   //......
 }

 public static void staicMethod(){
   //......
 }
 public static void main(String[] args) {
     Person person = new Person();
     // 调用实例方法
     person.method();
     // 调用静态方法
     Person.staicMethod()
 }
}

2、访问类成员是否存在限制

静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。


著作权归Guide所有 原文链接:Java基础常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)

和上一点类似,还是在说静态方法是不能调用非静态成员的,还是和静态方法和非静态方法在JVM中的生命周期的问题。

还有就是调用静态方法咱就直接通过类名调用,即 类名.方法名 这样的方式,调用非静态方法,也就是实例方法,就通过对象名,即 对象.方法名 的方式,这样不容易混淆。

面向对象

什么是面向对象

  • 面向对象编程(Object-oriented programming,OOP)

  • 面向对象编程的本质就是:以类的方式组织代码,以对象的形式组织(封装)数据

  • 抽象把许多事物的共同点抽取剥离出来,变成一个类

  • 三大特性

    • 封装
    • 继承
    • 多态
  • 从认识论的角度考虑是先有对象后有类,对象是以具体的事物。类是抽象的,是对对象的抽象的集合体

  • 从代码运行角度考虑是先有类后有对象。类是对象的模板。

构造方法有哪些特点?是否可被 override?

构造方法特点如下:

  • 名字与类名相同
  • 没有返回值,但不能用 void 声明构造函数。
  • 生成类的对象时自动执行,无需调用。

构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。


著作权归Guide所有 原文链接:Java基础常见面试题总结(中) | JavaGuide(Java面试 + 学习指南)

这里的话,我认为,正是因为构造方法的名字与类名是一样的,所以构造方法是不能被重写的,因为重写的要求是子类重写方法一定和父类方法名字不一样,而构造方法是可以被重载的,因为重载要求是重载方法名字一定和原方法名字一样

面向对象三大特征

封装

  • 该露的露,该藏的藏

    • 我们程序设计要追求**“高内聚 低耦合”**。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用 。
  • 封装(数据的隐藏)

    • 通常,应禁止直接访问一个对象中数据的实际表示,而通过操作接口来访问,这称为信息隐藏。
  • 记住这句话就够了:属性私有,get/set

继承

  • 继承的本质是对某一批类的抽象,子类继承父类,父类有子类都具有的共同点,从而实现对现实世界更好的建模。
  • extends意思是拓展,子类是父类的拓展。
  • Java类中只有单继承,**没有多继承!**单继承是指一个类只能继承自一个父类(即只能有一个直接父类)不过Java可以实现多个接口。
  • 继承是类和类之间的一种关系。除此之外,类和类之间的关系还有依赖、组合、聚合等。
  • 继承关系的两个类,一个为子类(派生类),一个为父类(基类)。子类继承父类,使用关键字extends来表示。
  • 子类与父类之间,从意义上讲应该具有"is a"的关系。
  • 子类继承父类,会拥有父类的所有属性和方法(包括私有),但是私有属性和方法只是拥有,无法访问!

多态

  • 多态能实现动态编译:类型,增强可拓展性。

  • 多态即同一方法可以根据发送对象的不同而采用多种不同的行为方式

  • 一个对象的实际类型是确定的,但可以指向对象的引用的类型有很多(父类、有关系的类)。

  • 多态存在的条件

    • 有继承关系
    • 子类重写父类方法
    • 父类引用指向子类对象
  • 注意:多态是方法的多态,属性没有多态性。

接口和抽象类有什么共同点和区别?

共同点

  • 都不能被实例化
  • 都可以包含抽象方法。
  • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

区别

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  • 一个类只能继承一个类,但是可以实现多个接口。
  • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

著作权归Guide所有 原文链接:Java基础常见面试题总结(中) | JavaGuide(Java面试 + 学习指南)

接口就是一个约束,一个类实现(implement)了某个接口,就就有了这个接口对应的行为约束;接口是规范,定义的是一组规则,实现了这个接口的类就要按照这个接口定义的规则来做出相应的行为。

Object

这几个问题我觉得还是很有的聊的,关于重写的equals()方法

== 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的:

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。

Objectequals() 方法:

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

equals() 方法存在两种使用情况:

  • 类没有重写 equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Objectequals()方法。
  • 类重写了 equals()方法:一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

举个例子(这里只是为了举例。实际上,你按照下面这种写法的话,像 IDEA 这种比较智能的 IDE 都会提示你将 == 换成 equals() ):

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 方法是被重写过的,因为 Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是对象的值。

当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

Stringequals()方法:

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

著作权归Guide所有 原文链接:Java基础常见面试题总结(中) | JavaGuide(Java面试 + 学习指南)

也就是说,没重写equals()方法,比较两个对象时,比较的是对象的内存地址是否相同;而重写了equals()方法后,比较的是==属性(值)==是否相等。这里我一开始是对hashCode和内存地址有混淆的,冲浪后,发现两者并没有必然的联系。

为什么要有 hashCode?

hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置

所以说hashCode和内存地址没有必然的联系。

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode

下面这段内容摘自我的 Java 启蒙书《Head First Java》:

这段内容解释的很清晰

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCodeHashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

其实, hashCode()equals()都是用于比较两个对象是否相等。

那为什么 JDK 还要同时提供这两个方法呢?

这是因为在一些容器(比如 HashMapHashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!

我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。

那为什么不只提供 hashCode() 方法呢?

这是因为两个对象的hashCode 值相等并不代表两个对象就相等

那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?

因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。

总结下来就是:

  • 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
  • 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
  • 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。

著作权归Guide所有 原文链接:Java基础常见面试题总结(中) | JavaGuide(Java面试 + 学习指南)

为什么重写 equals() 时必须重写 hashCode() 方法?

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

思考:重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。

总结

  • equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
  • 两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。

更多关于 hashCode()equals() 的内容可以查看:Java hashCode() 和 equals()的若干问题解答

也就是说,equals()hashCode()两个方法相辅相成,它们一起出现大大提高了检查的效率,两个相等的对象hashCode的值必须相等,只有equals()方法判断是不够的,因为存在两个对象的hashCode 值相等,但这两个对象不一定相等(哈希碰撞)的情况。

String

字符串常量池的作用了解吗?

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

著作权归Guide所有 原文链接:Java基础常见面试题总结(中) | JavaGuide(Java面试 + 学习指南)

字面值存储在字符串池(常量池)中,可以共享,用来避免字符串的重复创建。(JDK1.7,字符串池就在方法区中,JDK1.7后和静态变量一起被移到堆中,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。不同JDK版本所在位置不同)

这里关于JVM后面也要好好熟悉一下!

String、StringBuffer、StringBuilder 的区别?

可变性

String 是不可变的(后面会详细分析原因)。

StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 finalprivate 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
  	//...
}

线程安全性

String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilderStringBuilderStringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacityappendinsertindexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

著作权归Guide所有 原文链接:Java基础常见面试题总结(中) | JavaGuide(Java面试 + 学习指南)

总之,String能满足数据量少的情况,线程安全(不可变)。StringBuilder适用于单线程大量数据(线程不安全),StringBuffer适用于多线程大量数据(线程安全,有同步锁)关于锁后面还要好好熟悉一下。

String 为什么是不可变的?

String 类中使用 final 关键字修饰字符数组来保存字符串。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
	//...
}

修正:我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。

String 真正不可变有下面几点原因:

  1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

相关阅读:如何理解 String 类型值的不可变? - 知乎提问open in new window

补充(来自issue 675open in new window):在 Java 9 之后,StringStringBuilderStringBuffer 的实现改用 byte 数组存储字符串。

public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
    // @Stable 注解表示变量最多被修改一次,称为“稳定的”。
    @Stable
    private final byte[] value;
}

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    byte[] value;

}

Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?

新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。

JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。

Java面试基础篇_第2张图片

如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,bytechar 所占用的空间是一样的。

这是官方的介绍:https://openjdk.java.net/jeps/254 。


著作权归Guide所有 原文链接:Java基础常见面试题总结(中) | JavaGuide(Java面试 + 学习指南)

也就是说,

  1. 保存字符串的数组被 final 关键字修饰且为私有的,并且String 类没有提供修改这个字符串的方法;
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

综上两点,是String不可变的根本原因。

你可能感兴趣的:(#,Java部分,java,面试,开发语言)