Java面向对象

698 阅读50分钟

一、面向对象(上)

1、类与对象

1.1 类与对象

  • 类表示某群体的一些基本特征,对象表示一个个具体的事物(eg:学生,一个具体的同学)
  • 类是对象的模板,对象是类的实例
  • 对象是根据类创建的,一个类可有多个对象

1.2 局部变量与成员变量

  • 定义在中的变量为成员变量
  • 定义在方法中的变量为局部变量。

1.3 对象的创建与使用

class Student {
    String name; //声明姓名属性
    void read() {
        System.out.println("大家好,我是" + name + ",我在看书!");
    }
}
public class Test {
    public static void main(String[] args) {
        Student stu = new Student(); //创建并实例化对象(使用new开辟堆内存空间)
    }
}

对象名称stu保存在栈内存中,而对象的属性信息则保存在对应的堆内存中。

⑴ 堆内存:堆内存可以理解为一个对象的具体信息,每一个对象保存的只是属性信息,每一块堆内存的开辟都要通过关键字new来完成

⑵ 栈内存:可以理解为一个整型变量(只能够保存一个数值),其中保存的是一块(只能保存一块)堆内存空间的内存地址数值,但是为了方便理解,现在可以假设其保存的是对象的名字。

1.4 对象的引用与传递

类属于引用数据类型(ps:引用数据类型包括类、接口、数组),引用数据类型就是指内存空间可以被多个栈内存引用

小提示:一个栈内存空间只能指向一个堆内存空间,如果想要再指向其他堆内存空间,就必须先断开已有的指向后才能再分配新的指向。

引用传递

引用传递也称为传地址,指的是在方法调用时,传递的参数是按引用进行传递,其实传递的是引用的地址,也就是变量所对应的内存空间的地址。即形式参数和实际参数拥有相同的存储单元。在方法执行过程中,对形式参数的操作实际上就是对实际参数的操作,因此形式参数值的改变将会影响实际参数的值

值传递和引用传递可以简单理解为:

  • 值传递:函数接收到的是实际参数的“复印件”,也就是说函数对参数的任何修改都不会影响实际参数本身。
  • 引用传递:函数接收到的是实际参数的“地址”,也就是说函数对参数的修改实际上是在修改实际参数本身。

1.5 Java创建对象有哪些方式?【面试题】

1、new关键字

Person p1 = new Person();

2、运用反射手段,调用java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。

Class.newInstance

Person p1 = Person.class.newInstance();

Constructor.newInstance

Constructor<Person> constructor = Person.class.getConstructor();
Person p1 = constructor.newInstance();

3、调用对象的clone()方法

Person p1 = new Person();
Person p2 = p1.clone();

4、运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法

1.6 对象实体与对象引用有何不同?

  • 对象实体:是在内存中分配的具体的实体,包含了对象的属性和方法。(它的生命周期由JVM管理,即当对象实体不再被引用时,它将会被垃圾回收机制回收,内存中的对象实体将会消失)

    • 在Java中,通过new关键字为一个类创建对象实体。每一个对象实体都拥有自己独立的属性和方法,不同对象实体之间的属性和方法可以不同。
  • 对象引用:是指向对象实体的变量,它实际并不存储对象,而是存储对象的内存地址。通过引用,可以操作对象实体的属性和方法。(对象引用的生命周期由程序控制,当一个对象引用被赋予了null值或者超出其作用域时,它就无法再访问该对象实体。它的作用是用来操作对象的属性和方法)

    • 在Java中,可以使用赋值运算符将对象实体的引用赋值给变量,这样可以在不同的位置通过相同的引用访问同一个对象实体。

因此,对象实体表示了一个具体的对象,而对象引用则是一个指向该对象实体的变量。这两者之间的关系可以用一个比喻来形容:对象实体就像房子,而对象引用就像房子的门牌号码,它们指向同一个实体。

2、private、default、protected、public

image.png

  1. private:用于修饰类的属性和方法。类的成员变量一旦使用private,则该成员只能在本类中访问。
  2. default:若一个类中的属性或方法无任何访问权限声明,则该属性或方法就是默认的访问权限。可被本包中的其他类访问,不能被其它包的类访问。
  3. protected:一个类中的成员使用了protected,则只能被本包及不同包的子类访问。
  4. public:一个类的成员使用了public,则该成员可以在所有类被访问,不管是否在同一包中。
访问控制privatedefaultprotectedpublic
同一类中
同一包中的类
不同包的子类
全局范围

注意:局部变量是没有访问权限控制的,因为局部成员只在其所在的作用域内起作用,不可能被其他类访问到。

3、构造方法(构造器)

构造方法(也称为构造器)是类的一个特殊成员方法,在类实例化对象时自动调用,用于创建新的对象初始化对象的成员变量。它不需要被对象调用,而是在类实例化时自动执行,且只能执行一次。

每个类都有构造方法。如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法。

在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称与类名相等,一个类可以有多个构造方法。

这个时候可能就会有人讲了,我看到过很多类了,但是没有见到构造方法啊。这个时候就片面了哈,没有写不代表没有,而是被编译器给默认添加了一个默认的构造方法了。总在来讲Java中只有两种构造方法:有参构造无参构造

具体语法格式参考如下:

public class people {
    private String name;
    private int age;
    
    //无参构造
    public people() {
    }
    //有参构造方法
    public people(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

定义构造方法时有几点注意:

  1. 一个类中可以定义多个构造方法,构造方法的名称与类名相同
  2. 构造方法名称前不能有任何返回值类型声明,包括void
  3. 不能在构造方法中使用return返回一个值,但是可以单独写return语句作为方法的结束

构造方法的重载

一个类中可以定义多个构造方法,只要每个构造方法的参数类型或参数个数不同即可。

构造器是否可被重写(override)?

首先,构造器不能被继承,因为每个类名都不相同,而构造器的名称与类名相同,这肯定不能算是继承,所以,既然构造器不能被继承,那它肯定是不能被重写咯。

尽管构造器不能被重写,但子类可以通过调用父类的构造器(使用super()关键字)来继承和扩展父类的构造器行为。这样可以确保子类在实例化时能够正确地初始化其父类的成员变量。

注意

  1. 构造方法不可以被继承,不能被重写。
  2. 构造方法能被重载。
  3. 构造方法只能被public、[default]、protected、private 访问控制符修饰,不能被final、static、native、abstract、synchronized修饰。

4、重载(Overload)和重写(/覆盖)(Override)的区别?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。(重载是在编译时确定调用哪个方法,而重写是在运行时根据对象的实际类型调用对应的方法)

  • 重写发生在子类与父类之间,子类重新定义(覆盖)父类中的同名方法。具体来说,子类中的方法名称参数列表必须与父类中的方法相同。子类中重写的方法可以有不同的方法体,并且可以使用父类中的方法,添加新的行为以及修改行为。即外壳不变,核心重写!重写可以使子类具有更强的灵活性和扩展性,可以根据具体的需求来实现特定的功能。(子类方法返回值类型应与父类方法返回值类型相同或者是其子类型(基本数据类型-相同,引用数据类型-相同或者其子类),抛出的异常范围小于等于父类,访问修饰符范围大于等于父类

  • 重载是在同一个类中,定义具有相同名称不同参数列表的多个方法。(访问修饰符、)返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。

image.png

总的来说,重写是为了实现自己的行为,而重载是为了提供更多的灵活性和便利性。通常,重写是面向对象编程中实现多态性的一个重要手段,而重载则是函数式编程的一个重要手段。

重写的好处:重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

重载的好处:重载的好处是可以让函数具有更多的功能,增强实用性、灵活性和复用性,同时提高代码的可读性、可维护性和规范性,避免函数名称冲突和提高编程效率。

怎么重写父类方法?

重写父类方法,需要在子类中定义一个与父类方法名参数列表返回值类型相同的方法,并在方法体中编写新的实现代码。在代码中调用重写后的方法时,使用子类的实例对象调用该方法,将会执行子类中重写的代码。

重载的方法能否根据返回值类型进行区分?

不可以,Java重载的方法只能根据参数类型进行区分,因为不同的返回值类型并不影响方法的调用。如果有两个方法的名称、参数类型都相同,但是返回值类型不同,那么编译时将会出现错误,无法通过编译。

重写中子类抛出的异常必须和父类一样吗?可以不抛出或者抛出比父类大的异常类型吗?

在Java中,重写的方法可以不抛出任何异常。但是,如果子类方法抛出异常,子类抛出的异常范围小于等于父类。 这可以通过Java异常处理机制来理解。如果子类方法抛出的异常超过了父类方法抛出的异常类型,那么在子类被调用时,异常将由子类抛出,并且无法被父类捕获处理。这显然会导致编译错误。因此,子类中重写的方法抛出的异常必须是父类中声明的异常类型或其子类型。

5、this关键字

1. this和super有什么区别?this能调用到父类吗?

this 和 super 的区别:

  1. 指代的对象不同。super 指代的是父类,是用来访问父类的;而 this 指代的是当前类。
  2. 查找范围不同。super 只能查找父类,而 this 会先从本类中找,如果找不到则会去父类中找。
  3. 本类属性赋值不同。this 可以用来为本类的实例属性赋值,而 super 则不能实现此功能。
  4. this 可用于 synchronized。因为 this 表示当前对象,所以this 可用于 synchronized(this){....} 加锁,而 super 则不能实现此功能。

总结:this 和 super 都是 Java 中的关键字,都起指代作用,当显示使用它们时,都需要将它们放在方法的首行(否则编译器会报错)。this 表示当前对象,super 用来指代父类对象,它们有四点不同:指代对象、查找访问、本类属性赋值和 synchronized 的使用不同。

2. this() & super() 在构造方法中的区别?

  • 调用的对象不同:this() 用于调用当前类的其他构造方法,可以省略重复代码,简化开发;而 super() 用于调用父类的构造方法,用于初始化父类的属性值。
  • 调用的位置不同:this() 必须是构造方法中的第一行语句,用于调用当前类的其他构造方法;而 super() 也必须是构造方法中的第一行语句,用于调用父类的构造方法。
  • 参数传递的方式不同:this() 将当前对象隐式地作为参数传递给其他构造方法,可用于用当前对象初始化其他构造方法;super() 则将子类对象的引用显式地传递给父类构造方法,用于初始化父类中的属性值。
  • 返回类型不同:this() 没有返回类型,因为它只是用于在同一类中调用其他构造方法;而 super() 也没有返回类型,因为它只是初始化父类的属性,并不返回任何值。
  • 调用的方法不同:this() 调用当前类的其他构造方法,需要与该构造方法拥有相同的方法签名;而 super() 调用的是父类的构造方法,需要与该构造方法拥有相同的方法签名。

在 Java 的构造方法中,this() 关键字用于调用当前类的另一个构造方法,而 super() 关键字用于调用父类的构造方法。下面是一个简单的例子:

public class Animal {
    private String name;
    private int age;

    public Animal() {
        this("Unknown", 0);
    }

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class Dog extends Animal {
    private String breed;

    public Dog() {
        super();
        this.breed = "Unknown";
    }

    public Dog(String name, int age, String breed) {
        super(name, age);
        this.breed = breed;
    }
}

在这个例子中,Animal 类有两个构造方法,其中一个使用了 this() 关键字来调用另一个构造方法,并将默认值传递给它的参数。它的子类 Dog 类中有两个构造方法,其中一个调用了父类 Animal 的无参构造方法,另一个调用了父类 Animal 的有参构造方法,并将参数传递给它。

具体来说,Dog 类的构造方法通过 super() 调用了 Animal 类的构造方法,来初始化 name 和 age。同时,它还使用 this() 继续调用了自己的另一个构造方法,并将默认值 "Unknown" 传递给了 breed。需要注意的是,this() 和 super() 关键字必须在构造方法的第一行位置调用,以避免产生编译错误。

通过这个例子,我们可以看到 this() 和 super() 关键字在 Java 构造方法中的应用。this() 关键字可以用于调用当前类的另一个构造方法,并初始化当前对象的属性;而 super() 关键字则用于调用父类的构造方法,以初始化父类的属性。这两个关键字在 Java 中都有着重要的作用,特别是在面向对象的继承中,它们被广泛使用。

6、static关键字

1.说一说你对static关键字的理解

在类中,使用 static 修饰符修饰的属性(成员变量)称为静态变量,也可以称为类变量,常量称为静态常量,方法称为静态方法或类方法,它们统称为静态成员,归整个类所有

静态成员被类的所有实例(Java中实例通常是指对象)共享,就是说 static 修饰的方法或者变量不需要依赖于对象来进行访问,只要这个类被加载,Java虚拟机就可以根据类名找到它们。

调用静态成员的语法形式如下:

类名.静态成员

注意:

  • Java中,使用 static 关键字可以修饰类、方法和变量
  • static 修饰的成员变量和方法,从属于类;普通变量和方法从属于对象。
  • 静态方法不能调用非静态成员,编译会报错。

2.为什么要用static关键字?(作用)

用来声明同一类对象的共享资源(属性/方法)。

static关键字在Java中提供了一种方便的方式来共享变量执行与类本身相关的任务,而不是与特定对象的状态相关的任务。它主要用于创建静态变量、静态方法、静态代码块和静态内部类,使代码更简洁、高效。

(重点要记住:加static的变量叫做静态变量;静态变量在类加载时初始化,不需要new对象,静态变量的空间就开出来了;静态变量储存在方法区

具体而言static又可分为4种使用方式:

  1. 修饰成员变量。用static关键字修饰的静态变量在内存中只有一个副本。只要静态变量所在的类被加载,这个静态变量就会被分配空间,可以使用“类.静态变量”和“对象.静态变量”的方法使用。
  2. 修饰成员方法。static修饰的方法无需创建对象就可以被调用。static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和静态成员方法。
  3. 修饰代码块。JVM在加载类的时候会执行static代码块。static代码块常用于初始化静态变量。static代码块只会被执行一次。
  4. 修饰内部类。static内部类可以不依赖外部类实例对象而被实例化。静态内部类不能与外部类有相同的名字,不能访问普通成员变量,只能访问外部类中的静态成员和静态成员方法。

3.Java中是否可以覆盖/重写(override)一个private或者是static的方法?

static方法可以被继承和重载,但是不能被覆盖(override)。因为覆盖是基于动态绑定,而静态方法是不能动态绑定。在子类中定义一个与父类静态方法名相同的方法,不会覆盖父类中的静态方法,而是隐藏它。

private方法不能被继承和覆盖,因为它在父类中不可见。如果在子类中定义一个与父类中的private方法同名的方法,这个方法实际是一个新的方法,与父类中的方法无关。可以被重载

总之,static关键字表示静态的、属于类级别的成员,不能被覆盖(override);private方法也不能被覆盖或继承。

扩展:静态方法是否可以被重写?

静态方法是属于类的方法,它们被存储在类的字节码中,可以被多次调用。在 Java 中,静态方法和实例方法的命名方式和实现方式是一样的。但是,由于静态方法是属于类的方法,所以它们不具备多态的特性,即静态方法不能被覆盖(重写)。 示例代码:

class ParentClass { 
    static void staticMethod() { 
        System.out.println("This is ParentClass static method."); 
    } 
} 
class ChildClass extends ParentClass { 
// 重写父类中的 staticMethod() 方法 
    static void staticMethod() { 
        System.out.println("This is ChildClass static method."); 
    } 
} 
public class Main { 
    public static void main(String[] args) { 
        ParentClass p = new ChildClass(); 
        p.staticMethod();// 输出 "This is ParentClass static method." 
    } 
} 

p.staticMethod() 输出的结果是 "This is ParentClass static method." 而不是 "This is ChildClass static method.",这是因为静态方法与类直接相关联,而不是与类的实例相关联,因此在多态情况下也不能实现静态方法的重写

4.static修饰的类(静态类)能不能被继承?

参考答案:static修饰的类可以被继承。(但是不能重写)

静态类可以被继承,因为它和非静态类别无差别。被继承的静态类的子类并不需要实例化静态类本身,但如果在子类中访问了父类中的静态成员,则需要使用父类的类名来访问它们。

例如 ParentClassName.staticFieldName。 示例代码:

class ParentClass { 
    static class StaticInnerClass { 
    // Some static fields and non-static fields 
    } 
} 
class ChildClass extends ParentClass.StaticInnerClass { 
    // Some additional fields and methods 
} 

扩展阅读:

用static修饰内部类(也称为类内部类、静态内部类)(普通类是不允许声明为静态的,只有内部类才可以)。

static关键字的作用是把类的成员变成类相关,而不是实例相关即static修饰的成员属于整个类,而不属于单个对象。(理解就OK:外部类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关。因此static关键字不可修饰外部类,但可修饰内部类)

静态内部类需满足如下规则:

  1. 静态内部类可以包含静态成员,也可以包含非静态成员
  2. 静态内部类不能访问外部类的实例成员,只能访问它的静态成员;
  3. 外部类的所有方法、初始化块都能访问其内部定义的静态内部类
  4. 在外部类的外部,也可以实例化静态内部类,语法如下:

    外部类.内部类 变量名 = new 外部类.内部类构造方法();

5.简述内部类及其作用

定义:把类定义在另一个类的内部,该类就被称为内部类。

class Outer {
    class Inner {
    }
}

分类

  • 成员内部类:作为成员对象的内部类。可以访问private及以上外部类的属性和方法。外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法。外部类也可访问private修饰的内部类属性。
  • 局部内部类:存在于方法中的内部类。访问权限类似局部变量,只能访问外部类的final变量
  • 匿名内部类:只能使用一次,没有类名(故不能有构造器,构造器的名字必须与类名相同),只能访问外部类的final变量。
  • 静态内部类:使用 static 修饰,其他类可通过外部类.内部类来访问。类似类的静态成员变量。

内部类的作用

  1. 实现多重继承,因为 java 中类的继承只能单继承,使用内部类可达到多重继承;
  2. 内部类可以很好的实现隐藏,一般非内部类,不允许有 private 或 protected 权限的,但内部类可以;
  3. 减少了类文件编译后产生的字节码文件大小;

内部类是Java中一个非常有用且常见的概念。我们可以使用不同类型的内部类来实现各种不同的需求,例如实现嵌套类和接口,创建辅助功能或实现组件之间的通信。

6.能否在static中访问非static?

不可以。在 Java 中,静态(static)方法或静态代码块是属于类级别的,而非静态成员(非静态方法和非静态变量)是属于对象级别的。因此,在静态上下文中是无法直接访问非静态成员的,但可以通过创建对象实例来间接访问非静态成员。

注意:

  • 静态可以访问静态,但静态不能访问非静态而非静态可以访问静态
  • 在静态方法中可直接调用本类的静态方法,也可以通过类名.静态方法名的方式来调用其他类的静态方法
  • 静态方法不能直接调用实例方法和对象,但可以通过在静态方法中创建类的实例的方式间接调用

7、final关键字

1.final、finally、finalize的区别?(高频)

  • final是一个关键字,可以用来修饰类、方法或变量。当类被声明为final时,它不能被继承。当方法被声明为final时,它不能被重写。当变量被声明为final时,它只能被赋值一次(一旦被初始化便不可以被修改)。

  • finally也是一个关键字。作为异常处理的一部分,只能在try/catch语句中使用,finally附带一个语句块无论是否发生异常都会执行,经常被用在需要释放资源的情况下。(final可以修饰成员变量和局部变量)

  • finalize是Object类的一个方法,用于对象销毁前的清理工作,JVM会在垃圾回收器回收对象之前自动调用该方法。一般情况下,程序员不需要显式地调用该方法。

    (在垃圾收集器执行的时候会调用被回收对象的finalize()方法。当垃圾回收器准备好释放对象占用空间时,首先会调用finalize()方法,并在下一次垃圾回收动作发生时真正回收对象占用的内存)

2.如果final修饰了一个属性可以什么时候进行初始化?

final修饰的属性可以在定义时进行初始化,或者在类的构造函数中进行初始化,但是必须保证初始化只能进行一次。

注意:被 final 修饰的任何形式的变量,一旦获得了初始值,就不可以被修改!

8、Java代码块执行顺序

  1. 父类静态代码块(只执行一次)
  2. 子类静态代码块(只执行一次)
  3. 父类构造代码块
  4. 父类构造函数
  5. 子类构造代码块
  6. 子类构造函数
  7. 普通代码块

9、构造方法、成员变量初始化以及静态成员变量三者的初始化顺序?

先后顺序:静态成员变量、成员变量、构造方法。

详细的先后顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。

二、面向对象(下)

面向对象是软件开发方法,一种编程范式。是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。

1、面向对象和面向过程的区别

  • 面向对象(OOP):把构成问题事务分解成各个对象,分别设计这些对象,然后将他们组装成有完整功能的系统。

    • 面向对象思想偏向于了解一个人,这个人的性格、特长是怎么样的,有没有遗传到什么能力,有没有家族病史
  • 面向过程(POP):分析出解决问题所需要的步骤,然后用函数按这些步骤实现,使用的时候依次调用就可以了。

    • 面向过程思想偏向于我们做一件事的流程,首先做什么,其次做什么,最后做什么

面向过程只用函数实现,面向对象是用类实现各个功能模块。面向对象开发的程序一般更易维护、易复用、易扩展。

  • 面向对象是一种将数据和操作数据的方法组合成对象的程序设计方法,注重事物的实体和交互关系,使得程序更加灵活、可扩展、易于维护。
  • 面向过程是一种将问题分解为多个步骤的程序设计方法,注重过程的顺序、控制和函数的调用,适用于简单的任务处理和流程控制。两者的区别主要在于程序设计角度不同,注重点和语法也不同。

2、面向对象四大特征(封装、继承、多态、抽象)

封装

封装就是把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。良好的封装能够减少耦合。

封装目的:

  • 隐藏类的实现细节;
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问;
  • 可进行数据检查,从而有利于保证对象信息的完整性;
  • 便于修改,提高代码的可维护性。

继承

继承是从已有的类中派生出新的类,新的类继承父类的属性和行为,并能扩展新的能力,大大增加程序的重用性和易维护性。在Java中是单继承的,也就是说一个子类只有一个父类。

关于继承注意点:

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。

多态

Java的多态是什么呢?其实就是一种能力——同一个行为具有不同的表现形式(指同样的方法或操作在不同的对象上可以有不同的表现和结果);换句话说就是,执行一段代码,Java 在运行时能根据对象的不同产生不同的结果。

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

多态分为编译时多态和运行时多态:

  • 编译时多态主要指方法的重载
  • 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定

运行时多态有三个条件:(1)继承;(2)覆盖(重写);(3)向上转型

Java多态的好处与弊端?如何解决多态的缺陷?

优点:

  1. 可扩展性:通过多态,代码可以容易地扩展和修改,而不会影响现有的代码。通过继承子类并重写其父类的方法,您可以添加新的功能或修改现有的行为。
  2. 灵活性:多态使代码更具灵活性,因为它允许您在运行时动态地确定对象的类型和执行相应的方法。这意味着您可以编写更通用的代码,可以处理许多不同类型的对象,从而使代码更加灵活和可维护。
  3. 代码复用:多态提供了代码重用的机会。通过创建一个通用的父类来实现代码重用,并通过子类进行继承和重写,在不同的场景中使用相同的代码来处理不同的对象。
  4. 降低了耦合性:多态还有助于降低代码之间的耦合性。通过使用父类的引用来处理子类对象,您可以减少代码中的直接依赖关系,从而使代码更加松散耦合,更容易理解和维护。

缺点:

  1. 性能问题:多态是在程序运行时才能确定具体要调用哪个方法,需要进行动态绑定,因此会涉及到额外的运行时开销内存开销。相比于直接调用具体类的方法,多态会稍微降低程序的运行效率。
  2. 可读性问题:多态使得代码比较灵活,但也可能使得代码变得难以理解。尤其是在代码规模比较大时,如果使用多态的过度,可能将代码中的实际行为分散在各个不同的方法中,降低代码的可读性。
  3. 隐藏细节:当不同类中具有相同方法时,使用多态可能会隐藏实际调用的是哪个具体类的方法,使得代码的逻辑不清晰。在这种情况下,可能需要使用显式类型转换等方式来保证代码的正确性。

解决多态缺陷的方法:

  1. 缓存对象的类型:因为多态涉及到运行时的动态绑定,会带来一定的性能损失,所以可以在代码中缓存对象的类型信息,从而减少运行时动态绑定的开销。
  2. 显式类型转换:在某些情况下,我们不能使用多态来引用某些成员变量,为了解决这个问题,可以使用显式类型转换,将对象转换为具体类型,然后可以访问成员变量。
  3. 优化多态使用:在代码中合理使用多态,将具有相同方法的类集合在一起,避免出现代码冗余,同时也可以避免对性能的影响。
  4. 使用静态绑定:在一些特定情况下,我们可以使用静态绑定,避免动态绑定带来的运行时性能损失。比如,收到非法的对象类型,或者无法确定对象的具体类型时,可以使用静态绑定,直接调用具体类中的方法。
  5. 使用优化工具:在编译和运行程序的过程中,可以使用一些优化工具,比如JIT编译器、JVM垃圾收集器等,来优化程序的性能和内存使用效率,从而缓解多态带来的性能问题。

抽象

抽象:抽象是将一类对象的共同特征总结出来构造类的过程。包括数据抽象和行为抽象两方面。

抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么

抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面:一是过程抽象;二是数据抽象。(另一种解释)

JDK中对封装、继承、多态的典型应用?

  • 封装:在JDK中,封装被广泛应用于类和接口的定义,比如访问修饰符(private、protected、public)就是封装的体现,可以限制对类和对象的访问权限。例如,在Java中,String类就是被封装好的字符串操作类,对于用户来说,只需简单调用其公共方法即可实现对字符串的操作。

  • 继承:在JDK中,继承被广泛应用于类的继承、接口的继承、异常的继承等方面。比如,JDK中的各种类库可以在其他项目中被继承和覆盖,从而实现代码的重用和扩展。

  • 多态性:在JDK中,多态被广泛应用于方法的重载和重写、接口的实现、继承关系等方面。例如,我们在使用Java的集合类时,可以用List、Set或Map等多种数据结构,但是它们都具有相同的操作和方法,从而实现对数据的多态,使得程序更加灵活。

3、接口和抽象类

1.什么是抽象类

包含一个或多个抽象方法的类就是抽象类,抽象方法即没有方法体的方法,抽象方法和抽象类都必须声明为 abstract。例如:

// 抽象类
public abstract class Person {
    // 抽象方法
	public abstract String getDescription();
}

切记!除了抽象方法之外,抽象类还可以包含具体数据和具体方法

抽象类不能被实例化。也就是说,如果将一个类声明为 abstract, 就不能创建这个类的对象

new Person("Jack"); // Error

可以定义一个抽象类的对象变量, 但是它只能引用非抽象子类的对象。 假设 Student 类是 Person 的非抽象子类:

Person p = new Student("Jack"); // Right

所谓非抽象子类就是说,如果创建一个继承抽象类的子类并为之创建对象,那么就必须为父类的所有抽象方法提供方法定义如果不这么做(可以选择不做),子类仍然是一个抽象类(不可以被实例化,也就是不可以new一个对象),编译器会强制我们为新类加上 abstract 关键字。

下面定义扩展抽象类 Person 的具体子类 Student

public class Student extends Person { 
    private String major; 
    public Student(String name, String major) { 
        super(name); 
        this.major = major; 
    } 
    @Override
    public String getDescription(){ // 实现父类抽象方法
    	return "a student majoring in " + major; 
    } 
} 

Student 类中实现了父类中的抽象方法 getDescription 。因此,在 Student 类中的全部方法都是非抽象的, 这个类不再是抽象类。

👇 调用如下:

Person p = new Student("Jack","Computer Science");
p.getDescription();

由于不能构造抽象类 Person的对象, 所以变量 p 永远不会引用 Person 对象, 而是引用诸如 Student这样的具体子类对象, 而这些对象中都重写了 getDescription方法。

2.什么是接口

接口的本质其实也是一个类,而且是一个比抽象类还要抽象的类。怎么说呢?抽象类是能够包含具体方法的,而接口杜绝了这个可能性(注意!),在Java 8之前,接口非常纯粹,只能包含抽象方法,也就是没有方法体的方法(接口允许定义成员,但必须是常量)。而Java 8开始允许接口包含默认方法和静态方法。

Java 使用关键字 interface创建接口。和类一样,通常我们会在关键字 interface 前加上 public 关键字,否则接口只有包访问权限,只能在接口相同的包下才能使用它。

public interface Concept {...}

同样的,接口中既然存在抽象方法,那么它就需要被扩展(继承)。使用 implements 关键字使一个类扩展某个特定接口(或一组接口),通俗来说:接口只是外形,现在这个扩展子类要说明它是如何工作的。

class Implementation implements Concept {...}

注意,你可以选择显式地声明接口中的方法为 public,但是即使你不这么做,它们也是 public。所以当实现一个接口时,来自接口中的方法必须被定义为 public。否则,它们只有包访问权限,这样在被继承时,它们的可访问权限就被降低了,这是 Java 编译器所不允许的。

另外,接口中是允许出现常量的,与接口中的方法都自动地被设置为 public一样,接口中的域将被自动被设置为 public static final 类型,例如:

public interface Concept {
	void idea1(); // public void idea1();
    // 静态属性
	double item = 95; // a public static final constant
}

接口和类其中不同的一点就是,我们无法像类一样使用 new 运算符来实例化一个接口(因为接口连具体的构造方法都没有,肯定是无法实例化的)。尽管不能构造接口的对象,声明接口的变量还是可以的,接口变量必须引用实现了接口的类对象。

如同使用 instanceof 检查一个对象是否属于某个特定类一样, 也可以使用 instanceof 检查一个对象是否实现了某个特定的接口:

if(x instanceof Concept){
	...
}

注意:

  1. 类与接口之间的关系为实现,既可以单实现,也可以多实现
  2. 接口与接口之间的关系为继承,既可以单继承,也可以多继承

3.接口和抽象类的区别?

  1. 接口只有定义,不能有方法的实现(接口中的方法必须是抽象的,没有方法体),但java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。(接口是公开的,不能有私有的方法或变量,接口中的所有方法都没有方法体

  2. 接口使用关键字 interface 来定义(/创建)。抽象类使用关键字 abstract 来定义。

  3. 接口使用 implements 关键字定义其具体实现。抽象类使用 extends 关键字实现继承。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。

  4. 接口强调特定功能的实现,而抽象类强调所属关系

  5. 接口方法默认修饰符是public,并且不允许定义为private或protected(但接口中的字段必须是公共的、静态的和最终的(final)),抽象方法可以是任意访问修饰符修饰(抽象方法就是为了被重写所以不能使用private关键字修饰!),抽象类无限制。

  6. 接口被用于常用的功能,便于日后维护和添加删除,而抽象类更倾向于充当公共类的角色,不适用于日后重新对立面的代码修改。从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。

  7. 接口不能有构造器,抽象类可以有构造器。

总的来说,抽象类和接口都是实现代码重用的重要手段,但抽象类更加强调代码的继承性和代码复用性,而接口更加强调代码的隔离性和扩展性。抽象类常常被用于实现子类的共有属性和行为,而接口常常被用于定义行为契约和实现多态。

注意:抽象类是可以有私有方法或私有变量的,通过把类或者类中的方法声明为abstract来表示一个类是抽象类,被声明为抽象的方法不能包含方法体。子类实现方法必须含有相同的或者更低的访问级别(public->protected->private)。抽象类的子类为父类中所有抽象方法的具体实现,否则也是抽象类。

4.接口和抽象类的相同点

  • 都不能被实例化
  • 接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。

5.既然有了抽象类,为什么 Java还要不辞辛苦地引入接口?

因为一个类可以实现多个接口,但是一个类只能继承一个父类。正是接口的出现打破了 Java 这种单继承的局限,为定义类的行为提供了极大的灵活性。

class Implementation implements Concept1, Concept2 // OK

有一条实际经验:在合理的范围内尽可能地抽象。显然,接口比抽象类还要抽象。因此,一般更倾向使用接口而不是抽象类。

6.接口中可以有构造函数吗?

参考答案:由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。

7.面向接口编程的理解

是一种编程思想,其核心是面向抽象编程。它强调的是,只关注对象应该做什么,而不关注对象具体是如何实现的。通过定义接口抽象出一些功能集,然后通过实现这些接口来实现具体的功能,降低了程序之间的耦合性。

面向接口编程的优点有很多:

  1. 利于维护和升级:通过定义接口可确保对象之间的通信,使得程序结构更加灵活,可以更好地维护和升级。
  2. 代码重用:接口是一种标准,同一个接口可以被多个对象实现,从而实现代码重用。
  3. 降低耦合性:通过接口的实现,不同的对象之间可以进行通信,降低了对象之间的耦合性。
  4. 提高代码可读性和可维护性:面向接口编程可以使代码更容易理解和更容易维护,因为可以更好地组织代码和分离职责。

总之,面向接口编程可以提供更灵活、可维护和可重用的代码。随着软件工程趋于复杂化,面向接口编程已成为了现代软件工程的必要思想。

补充: 【两个示例代码】

抽象类 Animal 的示例代码:

image.png

抽象类 Animal,它有一个私有字段 name,一个带有参数的构造函数和两个抽象的方法 getSound() 和 getSpecies(),它们没有方法体并且是抽象的,这意味着它们必须在派生类中实现。该抽象类 Animal 可以代表所有类型的动物,并且派生类必须实现在此抽象类中定义的抽象方法。

接口 Flyable 的示例代码:

image.png

接口 Flyable,它定义了两个没有实现的方法 fly() 和 getSpeed()。任何类都可以实现该接口,并在其实现中添加适当的方法体。在这个示例中,该接口定义了可飞行的行为,实现它的类必须实现飞行并返回速度。

需要注意的是,抽象类可以包含字段和具有默认实现的方法,而接口只能包含常量和抽象方法(即没有方法体的方法)。另外,类只能继承一个抽象类,但是可以实现多个接口

8.抽象类(abstract class)和接口(interface)的应用场景

当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。

  1. 抽象类的使用场景

    • 既想约束子类具有共同的行为(但不在乎其如何实现),又想拥有缺省的方法,又能拥有实例变量
    • 如:模板方法设计模式,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中某些步骤的具体实现。
  2. 接口的应用场景

    • 约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现
    • 作为能够实现特定功能的标识存在,也可以是什么接口方法都没有的纯粹标识。
    • 实现类需要具备很多不同的功能,但各个功能之间可能没有任何联系。
    • 使用接口的引用调用具体实现类中实现的方法(多态)

4、compator和compatable的区别

Comparable是一个接口,用于对象内部的比较方式,该接口需要实现的方法是:

public interface Conparable<T>{
    public int compareTo(T o);
}

Comapator 也是一个接口,该接口有个compare方法,该接口需要实现的方法是:

public interface Conparator<T> {
    int compare(T o1, T o2);
}

除该方法外,comparator还可以实现其他方法。Comparator 除了可以通过创建自定义比较器外,还可以通过匿名类的方式,更快速、便捷的完成自定义比较器的功能。

Comparable和Comparator都是用来实现元素排序的,它们二者的区别如下

  • Comparable是“比较”的意思,而Comparator是“比较器”的意思;
  • Comparable是通过重写compareTo方法实现排序的,而Comparator是通过重写compare方法实现排序的;
  • Comparable必须由自定义类内部实现排序方法,而Comparator是外部定义并实现排序的。

所以用一句话总结二者的区别:Comparable可以看作是“对内”进行排序接口,而Comparator是“对外”进行排序的接口。

具体可参考:面试官:元素排序Comparable和Comparator有什么区别?

5、一个Java文件里可以有多个类吗(不含内部类)

  1. 一个java文件里可以有多个类,但最多只能有一个被public修饰的类;
  2. 如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。

6、异常

Throwable 类是 Java 语言中所有错误或异常的超类。

image.png

  • Error:虚拟机本身的错误(如系统崩溃、虚拟机错误等),应用程序无法处理
  • Exception:称为异常类,它表示程序本身可以处理的问题。
    • RuntimeException(运行时异常):指在程序运行期间可能抛出的异常,它不需要在编译期间捕获或声明。通常由程序员编程错误导致。例如,空指针异常、数组越界异常、类型转换异常等。
    • 非RuntimeException:必须进行异常处理,否则编译器会报错。通常由外部环境导致,例如文件找不到、网络超时等。

请介绍Java的异常接口?

Throwable是异常的顶级接口,两个直接子类:Error、Exception。...

再细讲Error及Exception,及其接口

1、捕获异常、多重捕获块

捕获异常

try/catch 代码块放在异常可能发生的地方。try/catch代码块中的代码称为保护代码。在try块中可能会抛出异常,而在对应的catch块中捕获和处理异常,从而解决异常的问题。

try {
   // 程序代码
} catch(ExceptionName e1) {
   //catch 块
}

catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。

如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块。

多重捕获块

一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。

try{
   // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}catch(异常类型3 异常的变量名3){
  // 程序代码
}...

可以在 try 语句后面添加任意数量的 catch 块。如果保护代码中发生异常,异常被抛给第一个 catch 块。如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。如果不匹配,它会被传递给第二个 catch 块。如此,直到异常被捕获或者通过所有的 catch 块。

try-catch,catch捕获到异常,如果没有抛出异常语句(throw),不影响后续程序。

2、throws/throw关键字

throw 和 throws 关键字用于处理异常。throw 用于在代码中抛出异常,而throws用于在方法声明中指定可能会抛出的异常类型

  • throw 关键字用于在当前方法中抛出一个异常。通常情况下,当代码执行到某个条件下无法继续正常执行时,可以使用 throw 关键字抛出异常,以告知调用者当前代码的执行状态。

    throw 异常对象;
    //eg:throw e;
    
  • throws 关键字用于在方法声明中指定该方法可能抛出的异常。当方法内部抛出指定类型的异常时,该异常会被传递给调用该方法的代码,并在该代码中处理异常(使用throws声明的方法表示此方法不处理异常,而交给方法的调用者进行处理)。

    修饰符 返回值类型 方法名(参数列表) throws 异常类1,异常类2…{  
    //eg:public void function() throws Exception{...}
    }
    

throw和throws的区别:

  1. throw用在方法体内,跟的是异常对象;throws用在方法声明后面,跟的是异常类型。
  2. throw只能抛出一个异常对象名;throws可以跟多个异常类名,用逗号隔开。
  3. throw 是在方法中出现不正确情况时,手动来抛出异常,结束方法的,执行了 throw 语句一定会出现异常。而 throws 是用来声明当前方法有可能会出现某种异常的,如果出现了相应的异常,将由调用者来处理,声明了异常不一定会出现异常。
  4. throw表示抛出异常,由方法体内的语句处理;throws表示抛出异常,由该方法的调用者来处理(它本身不处理异常)。

3、finally关键字

  • 无论是否发生异常,finally 代码块中的代码总会被执行。
  • 在try-catch语句中。finally 通常用于释放资源或清理工作。
try{
  // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}finally{
  // 程序代码
}

注意:

  • catch 不能独立于 try 存在。
  • 在 try/catch 后面添加 finally 块并非强制性要求的。
  • try 代码后不能既没 catch 块也没 finally 块。
  • 可以直接用 try-finally。
  • try, catch, finally 块之间不能添加任何代码。

finally语句块中的代码在什么情况下不会被执行?

  • 在执行try块时发生了System.exit方法调用。
  • 在执行try块时发生了死循环或死锁。
  • 在执行try块时,程序被操作系统强制终止。

其它情况下,在try/catch/finally语句执行的时候,try块先执行,当有异常发生,catch和finally进行处理后程序就结束了,当没有异常发生,在执行完finally中的代码后,后面代码会继续执行。值得注意的是,当try/catch语句块中有return时,finally语句块中的代码会在return之前执行。如果try/catch/finally块中都有return语句,finally块中的return语句会覆盖try/catch模块中的return语句。

4、try-with-resource

try-with-resource是JDK 7中引入的,是Java中的一个语法糖,用于简化资源的管理。它的作用是在程序执行完毕或抛出异常时,自动关闭打开的资源。

try (Resource resource = new Resource()) {  
// 使用资源的代码  
} catch (Exception e) {  
// 处理异常的代码  
}

Resource是资源的类型。

5、异常的处理方式

(抛出异常(throw、throws)、捕获异常(try-catch))

  1. try-catch
  2. throws声明
  3. throw关键字
  4. finally

6、说说Java的异常机制

回答点:描述异常类型、如何处理异常、解释finally块的作用、说明Java 7中的try-with-resources语句...

7、finally是无条件执行的吗?

参考答案:不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。

注意事项:如果在try块或catch块中使用 System.exit(1); 来退出虚拟机,则finally块将失去执行的机会。但是我们在实际的开发中,从来都不会这样做,所以尽管存在这种导致finally块无法执行的可能,也只是一种可能而已。

8、在finally中return会发生什么?

参考答案:在通常情况下,不要在finally块中使用return、throw等导致方法终止的语句,一旦在finally块中使用了return、throw语句,将会导致try块、catch块中的return、throw语句失效。

详细解析:当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块。只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句;如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里的任何代码。

9、简述StackOverFlowError

StackOverFlowError是Java中的一个异常类型,表示堆栈溢出错误。当一个方法被递归调用时,每次调用都需要在栈中分配一定的内存,如果递归调用的次数太多,就会导致栈中的内存空间不足,从而抛出StackOverFlowError异常。

这个错误通常发生在递归调用时,如果递归方法没有结束条件、结束条件不正确或递归深度过深等,都有可能导致栈内存不足导致堆栈溢出。为了避免这种情况发生,需要确保递归方法的结束条件正确,并且递归的深度不会太深。如果必要的话,还可以增加堆栈内存的大小来减少StackOverFlowError的发生。

8、为什么try-catch 块的消耗性能比较大

try-catch 块的消耗性能比较大,主要是因为在程序执行过程中,当异常发生时,try-catch 块会进行异常捕获和处理。如果异常没有被捕获,它会一直向上层抛出,直到被捕获或者到达程序顶层。这个过程需要不断地创建和销毁异常对象,以及查找和执行相应的异常处理器,这些操作都会消耗一定的时间和内存。

因此,在代码中过多地使用 try-catch 块,特别是在循环等需要高效执行的代码段中使用,会导致程序的性能下降。为了避免这种情况,应该尽可能地将 try-catch 块放在最小的代码块中,只捕获必要的异常,并且尽量避免在循环中使用。此外,也可以考虑使用其他技术来优化代码,例如使用条件判断来代替异常处理。

三、Java API