代码改变世界

Java基础知识常见面试题汇总 第二篇

2019-05-13 16:56  申城异乡人  阅读(2163)  评论(1编辑  收藏  举报

1. JDK,JRE,JVM三者之间的联系和区别

你是否考虑过我们写的xxx.java文件被谁编译,又被谁执行,又为什么能够跨平台运行?

1.1 基本概念

JVM:Java Virtual Machine,Java虚拟机。

JVM并不能识别我们平时写的xxx.java文件,只能识别xxx.class文件,它能够将class文件中的字节码指令进行识别并调用操作系统上的API完成指定的动作。

所以,JVM是Java能够跨平台的核心。

JRE:Java Runtime Environment,Java运行时环境。

JRE主要包含2个部分,JVM的标准实现和Java的一些基本类库。相比于JVM,多出来的是一部分Java类库。

JDK:Java Development Kit,开发工具包。

JDK是整个Java开发的核心,它集成了JRE和一些好用的小工具,例如:javac.exe,java.exe,jar.exe等。

上一篇博客中也提到了,我们可以通过javac命令将xxx.java文件编译为xxx.class文件。

1.2 联系和区别

了解完3者的基本概念,我们可以看出来3者的关系为一层层嵌套,即:JDK > JRE > JVM。

这里,我们提出一个问题:为什么我们安装完JDK后会有两个版本的JRE?

我电脑安装的JDK是1.8版本,安装完的目录如下图所示:

而jdk目录下也有1个jre:

我电脑的环境变量配置的是:

JAVA_HOME C:\Program Files\Java\jdk1.8.0_191

Path变量最后添加的是%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin。

也就是说,我电脑用的是jdk目录下的jre,而不是和jdk同级目录下的jre,也许大部分人都是这样的,可能没人注意,说实话,我之前还真没在意,看了网上的文章才知道,看来真的是要多问为什么。

这两个不同版本的JRE其实没什么联系,你可以修改下Path变量,指向任意1个都可以,只是很多人在安装JDK的时候,并不清楚JDK和JRE的区别,所以都会安装,比如说我,哈哈。

在jdk的目录下,有一些可执行文件,比如说javac.exe,其实内部也是调用的java类,所以jdk目录下的jre既提供了这些工具的运行时环境,也提供了我们编写的Java程序的运行时环境。

所以,可以得出如下结论:

如果你是Java开发者,安装JDK时可以选择不安装JRE

如果你的机器只是用来部署和运行Java程序,可以不安装JDK,只安装JRE即可

1.3 Java 为什么能跨平台,实现一次编写,多处运行?

Java引入了字节码的概念,JVM只能识别字节码,并将它们解释到系统的API调用,针对不同的系统有不同的JVM实现,有Lunix版本的JVM实现,也有Windows版本的JVM实现,但是同一段代码在编译后的字节码是一致的,而同一段字节码,在不同的JVM实现上会映射到不同系统的API调用,从而实现代码不修改即可跨平台运行。

所以说Java能够跨平台的核心在于JVM,不是Java能够跨平台,而是它的JVM能够跨平台。

2. 接口和抽象类的区别

2.1 抽象方法

当父类的一些方法不确定时,可以用abstract关键字将其声明为抽象方法,声明语法如下:

public abstract double area();

抽象方法与普通方法的区别:

  1. 抽象方法需要用关键字abstract修饰

  2. 抽象方法没有方法体,即只有声明,而没有具体的实现

  3. 抽象方法所在的类必须声明为抽象类

  4. 抽象方法必须声明为public或者protected,不能声明为private

    因为如果为private,则不能被子类继承,子类便无法实现该方法,抽象方法也就失去了意义

2.2 抽象类

如果一个类包含抽象方法,则这个类是抽象类,必须由关键字abstract修饰。

抽象类是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情,即没由起到抽象类的意义。对于一个父类,如果它的某个方法在父类中没有具体的实现,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为abstract方法,此时这个类也就成为abstract类了。

抽象类与普通类的区别:

  1. 抽象类不能被实例化,即不能通过new来创建对象
  2. 抽象类需要用关键字abstract修饰
  3. 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。
  4. 抽象类除了可以拥有普通类的成员变量和成员方法,还可以拥有抽象方法

值得注意的是,抽象类不一定必须包含抽象方法,只是一般大家使用时,都包含了抽象方法

举个具体的例子,比如我们有一个平面图形类Shape,它有两个抽象方法area()和perimeter(),分别用来获取图形的面积和周长,然后我们有矩形类Rectangle和圆形类Circle,来继承抽象类Shape,各自实现area()方法和perimeter()方法,因为矩形和圆形计算面积和周长的方法是不一样的,下面看具体代码:

package com.zwwhnly.springbootaction;

public abstract class Shape {

    public abstract double area();

    public abstract double perimeter();
}
package com.zwwhnly.springbootaction;

public class Rectangle extends Shape {
    private double length;
    private double width;

    public double getLength() {
        return length;
    }

    public void setLength(double length) {
        this.length = length;
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    @Override
    public double area() {
        return getLength() * getWidth();
    }

    @Override
    public double perimeter() {
        return (getLength() + getWidth()) * 2;
    }
}
package com.zwwhnly.springbootaction;

public class Circle extends Shape {
    private double diameter;

    public double getDiameter() {
        return diameter;
    }

    public void setDiameter(double diameter) {
        this.diameter = diameter;
    }

    @Override
    public double area() {
        return Math.PI * Math.pow(getDiameter() / 2, 2);
    }

    @Override
    public double perimeter() {
        return Math.PI * getDiameter();
    }
}
public static void main(String[] args) {
    Rectangle rectangle = new Rectangle();
    rectangle.setLength(10);
    rectangle.setWidth(5);

    double rectangleArea = rectangle.area();
    double rectanglePerimeter = rectangle.perimeter();
    System.out.println("矩形的面积:" + rectangleArea + ",周长" + rectanglePerimeter);

    Circle circle = new Circle();
    circle.setDiameter(10);

    double circleArea = circle.area();
    double circlePerimeter = circle.perimeter();
    System.out.println("圆形的面积:" + circleArea + ",周长" + circlePerimeter);
}

输出结果:

矩形的面积:50.0,周长30.0

圆形的面积:78.53981633974483,周长31.41592653589793

2.3 接口

接口,是对行为的抽象,声明语法为:

package com.zwwhnly.springbootaction;

public interface Alram {
    void alarm();
}

可以看出,接口中的方法没有具体的实现(会被隐式的指定为public abstract方法),具体的实现由实现接口的类来实现,类实现接口的语法为(这里以ArrayList类为例):

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
}

可以看出,一个类可以实现多个接口。

如果一个非抽象类实现了某个接口,就必须实现该接口中的所有方法。

如果一个抽象类实现了某个接口,可以不实现该接口中的方法,但其子类必须实现。

2.4 抽象类和接口的区别

语法层面上的区别:

  1. 一个类只能继承一个抽象类,而一个类却可以实现多个接口
  2. 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法
  3. 抽象类可以提供成员方法的实现细节,而接口中的方法不可以
  4. 接口的方法默认是public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法
  5. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的
  6. 接口不能用new实例化,但可以声明,但是必须引用一个实现该接口的对象, 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

设计层面上的区别:

  1. 抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

    继承是一个 "是不是"的关系,而接口实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类

    必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行

    这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。

  2. 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。

    对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更

    而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

这里引用下网上的门和警报的例子,门都有open()和close()两个动作,此时我们可以通过抽象类或者接口定义:

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

    public abstract void close();
}

或者使用接口:

public interface Door {
    void open();

    void close();
}

现在我们需要门具有警报alarm功能,该如何设计呢?

你可能想到的2个思路为:

1)在抽象类中增加alarm()方法,这样一来,所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能。

2)在接口中增加alarm()方法,这样一来,用到报警功能的类就必须要实现接口中的open()和close()方法,也许这个类根本就不具备open()和close()这两个功能,比如火灾报警器。

从这里可以看出,Door的open(),close()和alarm()属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。

因此最好的设计方式是单独将报警设计为一个接口Alarm,包含alarm()行为,Door设计为单独的抽象类,包含open()和close()行为,再设计一个报警门继承Door类并实现Alarm接口:

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

    public abstract void close();
}
public interface Alarm {
    void alarm();
}
public class AlarmDoor extends Door implements Alarm {

    @Override
    public void alarm() {

    }

    @Override
    public void open() {

    }

    @Override
    public void close() {

    }
}

3. 重载与重写的区别

3.1 基本概念

重载(Overload):发生在1个类里面,是让类以统一的方式处理不同类型数据的一种手段,实质表现就是允许一个类中存在多个具有不同参数个数或者类型同名方法,是一个类中多态性的一种表现。

返回值类型可随意,不能以返回类型作为重载函数的区分标准

重载规则如下:

  1. 必须具有不同的参数列表
  2. 可以有不同的返回类型
  3. 可以有不同的访问修饰符
  4. 可以抛出不同的异常

重写(Override):发生在父子类中,是父类与子类之间的多态性,实质是对父类的函数进行重新定义,

如果子类中的方法与父类中的某一方法具有相同的方法名、返回类型和参数列表,则新方法将覆盖原有的方法,如需调用父类中原有的方法可使用super关键字调用。

重写规则如下:

  1. 参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载
  2. 返回类型必须一直与被重写的方法相同,否则不能称其为重写而是重载
  3. 访问修饰符的限制一定要大于等于被重写方法的访问修饰符
  4. 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常,比如父类方法声明了一个检查异常 IOException,在重写这个方法时就不能抛出 Exception,只能抛出 IOException 的子类异常

总之,重载与重写是Java多态性的不同表现,重写是父类与子类之间多态性的表现,而重载是一个类中多态性的表现

3.2 示例

其实JDK的源码中就有很多重载和重写的例子,重载的话,我们看下Math类的abs()方法,就有以下几种实现:

public static int abs(int a) {
    return (a < 0) ? -a : a;
}

public static long abs(long a) {
    return (a < 0) ? -a : a;
}

public static float abs(float a) {
     return (a <= 0.0F) ? 0.0F - a : a;
}

public static double abs(double a) {
     return (a <= 0.0D) ? 0.0D - a : a;
}

重写的话,我们以String类的equals()方法为例,基类中equals()是这样的:

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

而子类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;
}

我们再来看一个特殊的例子:

package com.zwwhnly.springbootaction;

public class Demo {

    public boolean equals(Demo other) {
        System.out.println("use Demo equals.");
        return true;
    }

    public static void main(String[] args) {
        Object o1 = new Demo();
        Object o2 = new Demo();
        Demo o3 = new Demo();
        Demo o4 = new Demo();

        if (o1.equals(o2)) {
            System.out.println("o1 is equal with o2.");
        }

        if (o3.equals(o4)) {
            System.out.println("o3 is equal with o4.");
        }
    }
}

输出结果:

use Demo equals.

o3 is equal with o4.

是不是和你预期的输出结果不一致呢,出现这个的原因是,该类的equals()方法并没有真正重写Object类的equals()方法,违反了参数规则,因此o1.equals(o2)时,调用的仍是Object类的equals()方法,即比较的是内存地址,因此返回false。而o3.equals(o4)比较时,因为o3,o4都是Demo类型,因此调用的是Demo类的equals()方法,返回true。

4. 成员变量和局部变量的区别

4.1 定义的位置不一样

成员变量:在方法外部,可以被public,private,static,final等修饰符修饰

局部变量:在方法内部或者方法的声明上(即在参数列表中),不能被public,private,static等修饰符修饰,但可以被final修饰

4.2 作用范围不一样

成员变量:整个类全都可以通用

局部变量:只有方法当中才可以使用,出了方法就不能再用

4.3 默认值不一样

成员变量:如果没有赋值,会有默认值(类型的默认值)

局部变量:没有默认值,使用前必须赋值,否则编译器会报错

4.4 内存的位置不一样

成员变量:位于堆内存

局部变量:位于栈内存

4.5 生命周期不一样

成员变量:随着对象创建而诞生,随着对象被垃圾回收而消失

局部变量:随着方法的调用或者代码块的执行而存在,随着方法的调用完毕或者代码块的执行完毕而消失

package com.zwwhnly.springbootaction;

public class VariableDemo {
    private String name = "成员变量";

    public static void main(String[] args) {
        new VariableDemo().show();
    }

    public void show() {
        String name = "局部变量";
        System.out.println(name);
        System.out.println(this.name);
    }
}

输出结果:

局部变量

成员变量

5. 字符型常量和字符串常量的区别

  1. 形式上: 字符常量是单引号引起的一个字符,字符串常量是双引号引起的若干个字符
  2. 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算,字符串常量代表一个地址值(该字符串在内存中存放位置)
  3. 占内存大小:字符常量只占一个字节,字符串常量占若干个字节

6. 参考

弄懂 JRE、JDK、JVM 之间的区别与联系

Java抽象类和抽象方法例子

深入理解Java的接口和抽象类

JAVA重写和重载的区别

JAVA中局部变量 和 成员变量有哪些区别

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

最最最常见的Java面试题总结——第二周