Java学习之旅(一)

Java必知必会

(1) 面向对象可以解释下吗?都有哪些特性?

面向对象是一种思想,可以将复杂问题简单化,让我们从执行者变为了指挥者。面向对象的三大特性为:封装,继承与多态。

  • 封装:将事物封装成一个类,减少耦合,隐藏细节。保留特定的接口与外界联系,当接口内部发生改变时,不会影响外部调用方。
  • 继承:从一个已知的类中派生出一个新的类,新类可以拥有已知类的行为和属性,并且可以通过覆盖/重写来增强已知类的能力。
  • 多态:多态的本质就是一个程序中存在多个同名的不同方法,主要通过三种方式来实现:
    • 通过子类对父类的覆盖来实现
    • 通过在一个类中对方法的重载来实现
    • 通过将子类对象作为父类对象使用来实现

关于封装

封装主要是为了增加程序的可读性,解耦合并且隐藏部分实现细节。让我们来看下边的案例,看看该如何实现封装。

案例:

public class Test {
    public static void main(String[] args) {
        Student student = new Student();
        student.name = "小明";
        student.age = 16;
        student.printStudentAge();
 
        Student student2 = new Student();
        student2.name = "小白";
        student2.age = 120;
        student2.printStudentAge();
     }
}
 
class Student {
    String name;
    int age;
 
    public void printStudentAge() {
        System.out.println(name + "同学的年龄:" + age);
    }
}

程序输出如下:

Java学习之旅(一)_第1张图片

我们看到小白同学的年龄120,(假设)不符合业务逻辑需要,所以我们需要做一些内部逻辑的处理。所以需要进行代码封装,将内部逻辑进行一个隐藏。

封装之后的代码如下:

Java学习之旅(一)_第2张图片

通过将Student这个类的name和age属性私有化,只有通过公共的get/set方法才能进行访问,在get/set方法中我们可以对内部逻辑进行封装处理,外部的调用方不必关心我们的处理逻辑。

关于继承:

我们需要注意Java中不支持多继承,即一个类只可以有一个父类存在。另外Java中的构造函数是不可以继承的,如果构造函数被private修饰,那么就是不明确的构造函数,该类是不可以被其它类继承的,具体原因我们可以先来看下Java中类的初始化顺序

  • 初始化父类中的静态成员变量和静态代码块
  • 初始化子类中的静态成员变量和静态代码块
  • 初始化父类中的普通成员变量和代码块,再执行父类的构造方法
  • 初始化子类中的普通成员变量和代码块,再执行子类的构造方法

如果父类构造函数是私有(private)的,则初始化子类的时候不可以被执行,所以解释了为什么该类不可以被继承,也就是说其不允许有子类存在。我们知道,子类是由其父类派生产生的,那么子类有哪些特点呢?

  • 子类拥有父类非private的属性和方法
  • 子类可以添加自己的方法和属性,即对父类进行扩展
  • 子类可以重新定义父类的方法,即方法的覆盖/重写

既然子类可以通过方法的覆盖/重写以及方法的重载来重新定义父类的方法,那么我们来看下什么是方法的覆盖/重写吧。(其实这也是一个高频的面试热身题目
 

覆盖(@Override):

覆盖也叫重写,是指子类和父类之间方法的一种关系,比如说父类拥有方法A,子类扩展了方法A并且添加了丰富的功能。那么我们就说子类覆盖或者重写了方法A,也就是说子类中的方法与父类中继承的方法有完全相同的返回值类型、方法名、参数个数以及参数类型。

Demo展示如下:

public class OverrideTest {
    public static void main(String[] args) {
        new Son().say();
    }
}
class Parent {
    public void say(){
        System.out.println("我是父类中的say方法");
    }
}
class Son extends Parent {
    @Override
    public void say(){
        System.out.println("我是子类中的say方法,我覆盖了父类的方法");
    }
}

我们可以看到,子类本身继承了父类的say方法,但是其想重新定义该方法的逻辑,所以就进行了覆盖。
 

关于多态:

通过方法的覆盖和重载可以实现多态,上边我们介绍了何为方法的覆盖,这里我们先来介绍下何为方法的重载:
 

重载:

重载是指在一个类中(包括父类)存在多个同名的不同方法,这些方法的参数个数,顺序以及类型不同均可以构成方法的重载。如果仅仅是修饰符、返回值、抛出的异常不同,那么这是2个相同的方法。

Demo展示如下:

public class OverLoadTest {
 
    public void method1(String name, int age){
        System.out.println("");
    }
    // 两个方法的参数顺序不同,可以构成方法的重载
    public void method1(int age, String name){
        System.out.println("");
    }
    //---------------------------------------------
    public void method2(String name){
        System.out.println("");
    }
    // 两个方法的参数类型不同,可以构成方法的重载
    public void method2(int age){
        System.out.println("");
    }
 
    //---------------------------------------------
    public void method3(String name){
        System.out.println("");
    }
    // 两个方法的参数个数不同,可以构成方法的重载
    public void method3(int age, int num){
        System.out.println("");
    }
}

在这里需要注意如下面试官的追问问题(高频)。

面试官追问:如果只有方法返回值不同,可以构成重载吗?

答:不可以。因为我们调用某个方法,有时候并不关心其返回值,这个时候编译器根据方法名和参数无法确定我们调用的是哪个方法。

举例:如果我们分别定义了如下的两个方法:

public String Test(String userName){ }
public void Test(String userName){ }

在调用的时候,直接 Test(“XiaoMing”); 那么就会存在歧义。

我们再来看看如何通过将子类对象作为父类对象使用来实现多态。

把不同的子类对象都当作父类对象来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。这样操作之后,父类的对象就可以根据当前赋值给它的子类对象的特性以不同的方式运作。

对象的引用型变量具有多态性,因为一个引用型变量可以指向不同形式的对象,即:子类的对象作为父类的对象来使用。在这里涉及到了向上转型和向下转型,我们分别介绍如下:
 

向上转型:
子类对象转为父类,父类可以是接口。
公式:Father f = new Son(); Father是父类或接口,Son是子类。若调用f中的方法,则运行的是子类中重写的方法。

向下转型:
父类对象转为子类。公式:Son s = (Son) f;

在向上转型的时候我们可以直接转,但是在向下转型的时候我们必须强制类型转换。并且,如案例中所述,该父类必须实际指向了一个子类对象才可强制类型向下转型,即其是以这种方式Father f = new Son()创建的父类对象。若以Father f = new Father()这种方式创建的父类对象,那么不可以转换向下转换为子类的Son对象,运行会报错,因为其本质还是一个Father对象。

(2)JDK,JRE和JVM的区别与联系有哪些?

答:三者的基本概念可以概括如下:

  • JDK(Java Development Kit)是一个开发工具包,是Java开发环境的核心组件,并且提供编译、调试和运行一个Java程序所需要的所有工具,可执行文件和二进制文件,是一个平台特定的软件
  • JRE(Java Runtime Environment)是指Java运行时环境,是JVM的实现,提供了运行Java程序的平台。JRE包含了JVM,但是不包含Java编译器/调试器之类的开发工具
  • JVM(Java Virtual Machine)是指Java虚拟机,当我们运行一个程序时,JVM负责将字节码转换为特定机器代码,JVM提供了内存管理/垃圾回收和安全机制等

区别与联系:

  • JDK是开发工具包,用来开发Java程序,而JRE是Java的运行时环境
  • JDK和JRE中都包含了JVM
  • JVM是Java编程的核心,独立于硬件和操作系统,具有平台无关性,而这也是Java程序可以一次编写,多处执行的原因

解析:

这也是一道Java面试中的基础题,因为我们学习Java都是从安装一个JDK开始的。上边有说Java程序具有平台无关性,可以做到一次编写,多处执行。那么Java的跨平台性是如何实现的呢?

我们知道,Java程序都是运行在Java虚拟机,即JVM之上。JVM屏蔽了底层操作系统和硬件的差异。我想大多数同学的Hello Word程序都是在文本文件中写的,然后我们通过javac来编译.java文件,生成了一个.class文件,最后再通过java命令来运行.class文件。其实这就是经历了一个先编译,再解释执行的过程,即先将java文件编译成了字节码.class文件,然后交给Java虚拟机解释成特定平台上的机器码。

另外一个与平台无关性的原因是,Java的语言规范中规定了基本数据类型的取值范围和行为在各个平台上是保持一致的。

我们做一个简单的总结:

Java语言的平台无关性是如何实现的?

  • JVM屏蔽了操作系统和底层硬件的差异
  • Java面向JVM编程,先编译生成字节码文件,然后交给JVM解释成机器码执行
  • 通过规定基本数据类型的取值范围和行为

学习了上边的内容,聪明的你肯定可以回答面试官的如下问题了。

面试官:Java语言是编译型还是解释型语言?

答:Java的执行经历了编译和解释的过程,是一种先编译,后解释执行的语言,不可以单纯归到编译性或者解释性语言的类别中。

(1)抽象类和接口有什么区别?

答:抽象类和接口的主要区别可以总结如下。

  • 抽象类中可以没有抽象方法,也可以抽象方法和非抽象方法共存
  • 接口中的方法在JDK8之前只能是抽象的,JDK8版本开始提供了接口中方法的default实现
  • 抽象类和类一样是单继承的;接口可以实现多个父接口
  • 抽象类中可以存在普通的成员变量;接口中的变量必须是static final类型的,必须被初始化,接口中只有常量,没有变量

解析:

基础题目,绝对的基础并且高频的面试考察点。在Java中,我们通过abstract来定义抽象类,通过interface关键字来定义接口。如下所示:

// 这是一个抽象类
abstract class Animal{
 
}
// 这是一个接口
interface Bird{
 
}

我们知道接口和抽象类中都可以定义抽象方法,然后交由其实现类来实现该抽象方法。来看如下的面试官追问。
 

追问:抽象类和接口应该如何选择?分别在什么情况下使用呢?

答:根据抽象类和接口的不同之处,当我们仅仅需要定义一些抽象方法而不需要其余额外的具体方法或者变量的时候,我们可以使用接口。反之,则需要使用抽象类,因为抽象类中可以有非抽象方法和变量。
 

默认方法:

既然说到了JDK8接口中的方法可以实现,那么我们来看下default方法的具体实现。我们先给出一个接口中的default方法Demo,如下所示:

public interface MyInterface {
    // 定义一个已经实现的方法,使用default表明
    default void say(String message){
        System.out.println("Hello "+message);
    }
    // 普通的抽象方法
    void test();
}

当一个类实现该接口时,可以继承到该接口中的默认方法,如下所示:

public interface MyInterface {
    // 定义一个已经实现的方法,使用default表明
    default void say(String message){
        System.out.println("Hello "+message);
    }
    // 普通的抽象方法
    void test();
}
 
class MyClass implements MyInterface{
    @Override
    public void test() {
        System.out.println("test...");
    }
}
class Main{
    public static void main(String[] args) {
        MyClass client = new MyClass();
        client.test();
        client.say("World...");
    }
}

这样的话,大家就会有疑问,如果两个接口中存在同样的默认方法,实现类继承的是哪一个呢?Demo如下:

public interface MyInterface {
    // 定义一个已经实现的方法,使用default表明
    default void say(String message){
        System.out.println("Hello "+message);
    }
    // 普通的抽象方法
    void test();
}
interface MyInterface2{
    // 定义一个已经实现的方法,使用default表明
    default void say(String message){
        System.out.println("[2]-Hello "+message);
    }
}
// 此处会编译错误
class MyClass implements MyInterface, MyInterface2{
    @Override
    public void test() {
        System.out.println("test...");
    }
}

这个时候,实现类那里会编译错误,错误如下:

Java学习之旅(一)_第3张图片

 

这个编译错误的大概意思就是说:有两个相同的方法,编译器不知道该如何选择了。我们有两种处理方式,如下所示:

  • 重写多个接口中的相同的默认方法
  • 在实现类中指定要使用哪个接口中的默认方法

重写多个接口中的相同的默认方法:

class MyClass implements MyInterface, MyInterface2{
    @Override
    public void say(String message) {
        System.out.println("[Client]-Hello "+message);
    }
    @Override
    public void test() {
        System.out.println("test...");
    }
}

在实现类中指定要使用哪个接口中的默认方法:

class MyClass implements MyInterface, MyInterface2{
    // 手动指定哪个默认方法生效
    public void say(String message) {
        MyInterface.super.say(message);
    }
    @Override
    public void test() {
        System.out.println("test...");
    }
}

那么JDK8中为什么会出现默认方法呢?

答:使用接口,使得我们可以面向抽象编程,但是其有一个缺点就是当接口中有改动的时候,需要修改所有的实现类。在JDK8中,为了给已经存在的接口增加新的方法并且不影响已有的实现,所以引入了接口中的默认方法实现。

默认方法允许在不打破现有继承体系的基础上改进接口,解决了接口的修改与现有的实现不兼容的问题。该特性在官方库中的应用是:给java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()等等。在我们实际开发中,接口的默认方法应该谨慎使用,因为在复杂的继承体系中,默认方法可能引起歧义和编译错误。

你可能感兴趣的:(java,学习,开发语言)