跳至主要內容

面向对象

摸鱼散人大约 11 分钟

⾯向对象和⾯向过程的区别?

  • ⾯向过程

    • 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的一次调用就可以。
  • ⾯向对象

    • 面向对象,把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事件在解决整个问题的过程所发生的行为。 目的是为了写出通用的代码,加强代码的重用,屏蔽差异性。

面向对象有哪些特性?

  • 封装
    • 封装把⼀个对象的属性私有化,同时提供⼀些可以被外界访问的属性的⽅法
  • 继承
    • 继承是使⽤已存在的类的定义作为基础创建新的类,新类的定义可以增加新的属性或新的方法,也可以继承父类的属性和方法。通过继承可以很方便地进行代码复用
  • 多态
    • 所谓多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编程时并不确定,⽽是在程序运⾏期间才确定
    • 即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定
    • 在 Java 中有两种形式可以实现多态:继承(多个⼦类对同⼀⽅法的重写)和接⼝(实现接⼝并覆盖接⼝中同⼀⽅法)

重载(overload)和重写(override)的区别?

  • 方法重载和方法重写都是实现多态性的方式

    • 区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性
  • 方法重载

    • 方法名相同,形参不同(参数个数、参数类型、参数顺序满足任一)则视为重载;
    • 与返回值和修饰符无关
    • 调用方法不同,与异常类型更没关系
    • 发生在同一个类中
  • 方法重写

    • 方法名相同,形参相同
    • 基本数据类型相同,若为引用数据类型则返回值为被重写方法的返回值或是其子类
    • 重写方法不能抛出范围更大的受控异常(父类不希望子类出现更多的异常)
    • 重写方法的访问权限可以更广(子类可以扩大方法的访问范围)
    • 发生在具有继承关系的父子类中

访问修饰符public、private、protected、以及不写(默认)时的 区别?

  • default (即默认,什么也不写)
    • 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
  • private
    • 在同一类内可见。可以修饰变量、方法。
    • 注意:不能修饰外部类
  • public
    • 对所有类可见。可以修饰类、接口、变量、方法
  • protected
    • 对同一包内的类和所有子类可见。可以修饰变量、方法。
    • 注意:不能修饰外部类

this关键字有什么作用?

  • 指向对象本身的一个指针
  • this的用法在Java中大体可以分为3种
    • 普通的直接引用,this相当于是指向当前对象本身
    • 形参与成员变量名字重名,用this来区分
    • 引用本类的构造函数

抽象类(abstract class)和接口(interface)有什么区别?

  • 方法实现
    • 接口中的方法默认是公开的(public),只能定义,不能实现。从JDK 8开始,接口中的方法可以有默认实现,而抽象类可以包含非抽象的方法
      • 如果同时实现两个接口,并且接口中定义了相同的默认方法,则必须重写方法,否则会报错
    • 在JDK 8中,接口也可以定义静态方法,并且可以直接使用接口名调用
      • 实现类和实例不能调用静态方法
    • JDK 9允许在接口中定义私有方法
  • 变量
    • 接口中只能包含静态(static)和常量(final)变量,不能有其他类型的变量。而抽象类中可以包含任意类型的变量
  • 实现
    • 一个类可以实现多个接口,但只能继承一个抽象类。接口自身可以通过extends关键字扩展多个接口
  • 方法修饰符
    • 接口中的方法默认修饰符是public,抽象方法可以有public、protected和default修饰符。抽象方法是为了被重写,所以不能使用private修饰符
  • 设计层面
    • 抽象类是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为规范

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

成员变量 = 实例变量 + 静态变量(类变量)

  • 语法形式
    • 成员变量是在类中声明的变量,可以被public、private、static等修饰符所修饰
    • 成员变量包括实例变量和静态变量(类变量)
      • 实例变量是每个对象独有的,而静态变量是类共享的
    • 局部变量是在方法中或代码块中声明的变量,不能被访问控制修饰符及static所修饰
  • 存储方式
    • 实例变量存储在堆内存中
    • 静态变量存储在方法区(元空间)的静态存储区,所有对象共享同一份静态变量的拷贝
    • 局部变量存在栈内存中,随着方法或代码块的执行而创建和销毁
  • 生存周期
    • 静态变量在类被加载至数据区后,只有当程序结束时才会被销毁,它的生命周期与程序的生命周期相同。
    • 实例变量即对象变量,随对象的创建而生,随对象的销毁而销毁
    • 局部变量只在方法或代码块内有效,一旦超出这个范围就不再有效
  • 默认值
    • 非final修饰的成员变量具有默认值,而局部变量没有默认值
    • 成员变量的默认值根据其类型而定,例如整型默认为0,布尔型默认为false,引用类型默认为null
    • 局部变量在使用前必须显式地进行初始化

静态变量和实例变量的区别?静态方法、实例方法呢?

  • 静态变量
    • 是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个副本
  • 实例变量
    • 必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存
  • 静态方法
    • static修饰的方法,也被称为类方法
    • 在外部调⽤静态⽅法时,可以使⽤"类名.⽅法名"的⽅式,也可以使⽤"对象名.⽅法名"的⽅式
    • 静态方法里不能访问类的非静态成员变量和方法
  • 实例⽅法
    • 依存于类的实例
    • 需要使用"对象名.⽅法名"的⽅式调用
    • 可以访问类的所有成员变量和方法

final关键字有什么作用?

  • 被final修饰的类不可以被继承
  • 被final修饰的方法不可以被重写
  • 被final修饰的变量不可变,被final修饰的变量必须被显式第指定初始值
    • 这里的不可变指的是变量的引用不可变,不是引用指向的内容的不可变

final、finally、finalize的区别?

  • final 用于修饰变量、方法和类
    • final修饰的类不可被继承
    • 修饰的方法不可被重写
    • 修饰的变量不可变
  • 在 try/catch 语句中,最终一定被执行
    • 经常被用在需要释放资源的情况下
    • System.exit (0) 可以阻断 finally 执行
  • finalize 是在 java.lang.Object 里定义的方法
    • 这个方法在 gc 启动,该对象被回收的时候被调用
    • 一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象
      • 不推荐使用
        • 有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法

==和 equals 的区别?

  • ==
    • 判断两个对象是不是同⼀个对象
      • 基本数据类型==比较的是值
      • 引⽤数据类型==比较的是内存地址
  • equals()
    • 判断两个对象是否相等
    • 默认情况类没有覆盖 equals() ⽅法,等价于通过“ == ”比较这两个对象

hashCode与 equals?

  • 什么是HashCode?
    • hashCode方法用于获取对象的哈希码,哈希码是一个整数,用来表示对象的唯一标识
    • 哈希码常用于哈希表等数据结构的实现中,可以快速定位对象
  • 为什么重写 quals 时必须重写 hashCode ⽅法?
    • 这是Java的规定,主要目的是确保 Java 中的集合类(例如 HashMap、HashSet 等)能够正确地工作并保持一致性
      • 对象相等性原则
        • 如果两个对象根据 equals() 方法判断相等,则它们的 hashCode() 方法的返回值必须相等
    • hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode() ,则该class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)

Java是值传递,还是引用传递?

  • Java语言是值传递
    • 当传递基本数据类型(如int、float、boolean等)时
      • 传递的是该值的副本,对形参的修改不会影响实参
    • 当传递对象引用时
      • 传递的是该引用的副本,而不是对象本身。因此,对形参引用的修改不会影响实参引用的指向,但可以通过引用修改对象的状态

什么是深拷贝和浅拷贝?

  • 浅拷贝
    • 仅拷贝被拷贝对象的成员变量的值,也就是基本数据类型变量的值,和引用数据类型变量的地址值,而对于引用类型变量指向的堆中的对象不会拷贝。
  • 深拷贝
    • 完全拷贝一个对象,拷贝被拷贝对象的成员变量的值,堆中的对象也会拷贝一份
  • 深拷贝是安全的
    • 浅拷贝的话如果有引用类型,那么拷贝后对象,引用类型变量修改,会影响原对象
  • 浅拷贝如何实现呢?
    • Object类提供的clone()方法可以非常简单地实现对象的浅拷贝
  • 深拷贝如何实现呢?
    • 重写克隆方法
      • 重写克隆方法,引用类型变量单独克隆,这里可能会涉及多层递归
    • 序列化
      • 可以先讲原对象序列化,再反序列化成拷贝对象

Java 创建对象有哪几种方式?

  • new创建新对象
  • 通过反射机制
  • 采用clone机制
  • 通过序列化机制
    • Java中序列化可以通过实现Externalizable或者Serializable来实现
    • 或者使用json序列化

Java内部类为什么推荐用静态类

在Java中,内部类(Inner Class)可以分为静态内部类(Static Inner Class)和非静态内部类(Non-static Inner Class)。推荐使用静态内部类的原因主要有以下几个方面:

  1. 避免内存泄漏
    非静态内部类会隐式地持有其外部类实例的引用。如果内部类的生命周期超出了外部类实例的生命周期,就会导致内存泄漏。而静态内部类不会持有外部类的引用,从而避免了这个问题。

  2. 独立性强
    静态内部类不依赖于外部类的实例,因此它可以独立存在,不需要通过外部类的实例来创建。这提高了类的独立性和可重用性。

  3. 性能优化
    静态内部类在编译时不需要持有外部类的引用,减少了不必要的内存开销,从而提升了性能。

  4. 线程安全
    静态内部类常常用于实现单例模式中的“静态内部类实现方式”,这种方式不仅实现简单,还能确保线程安全,同时具备延迟加载的特性。

示例

非静态内部类

public class OuterClass {
    private int outerField = 10;

    class InnerClass {
        public void print() {
            System.out.println("Outer field: " + outerField);
        }
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        InnerClass inner = outer.new InnerClass();
        inner.print();
    }
}

在这个例子中,InnerClass是非静态内部类,它持有OuterClass实例的引用。

静态内部类

public class OuterClass {
    private static int outerField = 10;

    static class StaticInnerClass {
        public void print() {
            System.out.println("Outer field: " + outerField);
        }
    }

    public static void main(String[] args) {
        StaticInnerClass inner = new StaticInnerClass();
        inner.print();
    }
}

在这个例子中,StaticInnerClass是静态内部类,它不持有OuterClass实例的引用,且可以直接访问外部类的静态成员。

总结

使用静态内部类有助于避免内存泄漏、提高类的独立性和性能,以及确保线程安全。在设计模式中,如单例模式的实现,静态内部类也具有明显的优势。因此,除非必须使用非静态内部类,否则推荐使用静态内部类。