Java:对象和类

164 阅读16分钟

面向对象基础

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

主要区别在于,对一个需求的解决方式不同。

  • 面向过程:把要处理的需求分成多个步骤、多个方法,通过执行一个个方法来完成需求

  • 面向对象:拿到一个需求,先设计抽象出多个类,用对象执行方法的方式完成需求,解决问题。

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

(虽然心里知道有区别,但是一旦面试问到,大脑还是会一片空白,所以还是整理一下)

new 运算符,new 创建对象实例(对象实例在
内存中),对象引用指向对象实例(对象引用存放在栈内存中)。

  • 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
  • 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。

对象的相等和引用相等的区别

  • 对象的相等一般比较的是内存中存放的内容是否相等。
  • 引用相等一般比较的是他们指向的内存地址是否相等。

如果一个类没有声明构造方法,该程序能正常执行吗

可以。

  • 如果没有手动添加构造方法,Java会默认添加一个无参的构造方法。这也就是为什么new一个对象的时候要带括号,实际上就是调用无参的构造方法。

  • 如果自己手动写了有参的构造方法,Java就不会添加无参的构造方法了,所以一般如果自己定义了构造方法,无参的构造方法也要写出来。

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

特点:

  • 方法名与类名相同

  • 没有返回值,但也不能用void声明构造方法;

  • new一个对象的时候会自动执行,无需调用。

构造方法不可以被重写(因为构造方法的方法名与类名相同,而重写又是子类重写父类的方法,这就要求子类和父类的类名相同,显然不存在这种可能性。),但是可以被重载。

面向对象的三大特征

封装

将具有相同属性和行为的事物封装成一个类,隐藏了实现细节,外界不可以随意访问和修改。适当的封装可以让代码更容易理解和维护,也加强了代码的安全性。

继承

  • 继承可以解决具有类似属性类的重复复制问题,如果有一个事物和已有的类相似,但不完全相同,这时候就可以使用继承,不需要重新创建一个相似的类。
  • 特点
    • 子类拥有父类中的一切,但是访问父类中的内容,需要受访问控制符的限制。而且不能继承父类的构造方法
    • 子类可以定义自己的属性和方法,可以对父类进行扩展
    • 子类可以覆盖父类的方法

多态

  • 多态就是一个对象在调用方法的时候,不确定是调用的哪个方法。
  • 多态分为静态多态(编译时多态)和动态多态(运行时多态)
    • 静态多态:指方法的重载,具体调用哪个方法,是根据传入参数类型确定的。编译时可以知道调用的是哪个方法。
    • 动态多态:如果子类重写了父类的方法,而且父类引用指向子类对象,那么在子类对象调用方法的时候,会出现多态的情况。子类对象可以调用哪些方法,是由引用类型决定的,实际调用哪个方法(子类还是父类的方法),是在代码执行过程中由子类对象类型决定的。
      • 如果子类覆盖了父类的方法,那么调用的是子类的方法
      • 如果子类没有重写父类的方法,就会去父类里找,父类没有就去父类的父类里找。

举例:

//queue对象能调用的方法就是队列的相关方法了,这就是一种多态
Queue<Integer> queue = new LinkedList<>()

//linkedList对象能调用的方法就不包含Queue中的方法了
LinkedList<Integer> linkedList = new LinkedList<>();

//deque对象能调用的方法就是Deque类中的方法
Deque<Integer> deque = new LinkedList<>();

多态的核心概念

为了更好地理解多态,我们需要掌握以下核心概念:

  1. 方法重写(Override): 子类可以提供对父类中已有方法的新实现。在子类中重新定义一个与父类中方法名、参数列表和返回类型相同的方法,从而覆盖(重写)了父类中的方法。
  2. 向上转型(Upcasting): 可以将子类的对象引用赋给父类类型的变量,这被称为向上转型。这样做可以让我们在父类引用上调用子类的方法,从而实现多态性。
  3. 动态绑定(Dynamic Binding): 运行时多态性的关键概念之一。它意味着方法的调用是在程序运行时根据对象的实际类型来确定的,而不是在编译时。
  4. instanceof 运算符: 用于检查一个对象是否是特定类的实例。它可以帮助我们在运行时确定对象的类型,从而进行适当的操作。

实现多态

要实现多态,需要满足以下条件:

  1. 存在继承关系,即有父类和子类。
  2. 子类必须重写父类的方法。这意味着子类提供了对父类方法的新实现。
  3. 父类的引用可以指向子类的对象,这是向上转型的体现。

重写和重载

什么是方法签名

方法的名称+参数列表(参数类型、顺序、数量),组成方法签名,一个类里面不能有多个签名相同的方法。

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

静态方法跟类的生存周期一样,在类加载的时候就会分配内存;但非静态成员变量,跟对象的生存周期一样,类加载的时候,非静态成员变量还不存在,故此时静态方法调用非静态成员变量会报错(编译的时候就报错)。

方法的重载和重写的区别

  • 重载:发生在同一个类或者子类和父类之间。有多个方法名称相同,参数类型/顺序/数量不同的方法
  • 重写:发生在子类和父类之间。遵循“两同两小一大”
    • 方法名、参数列表都必须相同;
    • 子类返回值类型范围 小于等于父类的返回值类型范围
      • 如果父类方法的返回值类型是void和基本类型,则子类方法的返回值类型不能改
      • 如果父类方法的返回值类型是引用类型,则子类方法的返回值类型可以是该引用类型的子类
    • 子类抛出的异常范围 小于等于父类的异常范围
    • 子类方法的访问权限 大于等于父类方法的访问权限

什么是可变长参数

  • 方法的形参个数不确定,调用方法时可以传入0-多个参数;如下:可以传入多个Integer类型或多个String类型
public String print(Integer... args){
    return "789";
}

public String print(String... args){
    return "123";
}
  • 可变参数只能放在方法的最后一个参数;
  • 遇到重载的情况,会优先匹配有固定参数的方法。

抽象类和接口的区别

接口的特点

接口中可以定义三种类型的方法

  • 抽象方法:默认是public abstract修饰,修饰符可以不写。不能有方法体。子类必须重写该方法。

  • 默认方法:使用default修饰(Java8),可以有方法体。子类可以不重写,子类对象会自动调用接口的default方法。

  • 静态方法:使用public static修饰(Java8),可以有方法体,子类不可重写

接口中的默认方法、静态方法可以同时有多个。 参考文章:Java8:接口里面可以写实现方法吗【可以】 、接口可以多继承吗【可以】

接口中定义的成员变量

  • 默认是public static final修饰的,也就是常量。

代码实践

public interface InterfaceTest {

    // java 8之后,接口可以定义public static方法(private不允许),使用时直接InterfaceTest.staticMethod()进行调用即可
    // Map接口下面就有许多静态方法,可以参考
    static void staticMethod() {
        System.out.println(123);
    }

    // java 8之后,接口可以定义default方法,使用时可通过子类对象(子类可以不重写该方法)调用该方法
    // Map接口下面就有许多静态方法,可以参考
    default String getName() {
        return "123";
    }

    //接口一般定义的都是这样的抽象方法,不能有方法体。其中public abstract可以省略
    public abstract void print();
}

子类实现上述接口

public class SubInterfaceTest implements InterfaceTest {
    //子类必须实现接口的抽象方法
    @Override
    public void print() {
        System.out.println("hidfhi");
    }

    public static void main(String[] args) {
        //接口的静态方法,直接通过接口名称.方法名进行调用
        InterfaceTest.staticMethod();
        // 接口的default方法,通过子类对象调用
        new SubInterfaceTest().getName();
    }

}

为什么接口的静态和抽象方法必须是public修饰的

接口必须要有实现类才有意义,所以必须是public的

为什么接口的成员变量默认是public static final修饰

  • 接口中的属性对所有实现类只有1份,所以是static的

  • 要使实现类向上转型(父类引用指向子类对象)成功?,属性必须是final的。

为什么要有接口默认方法

因为接口的抽象方法是必须要被子类重写的。如果之前已经写好的接口,新加了一个抽象方法,那所有实现类都要重写该方法。而且有些实现类可能并不需要该方法,增加了代码的维护成本。接口里面的默认方法,实现类是可以自动继承的,不需要改动任何实现类,可以解决上述问题。

接口的默认方法,也可以被实现类重写。

为什么要有接口静态方法

接口静态方法和默认方法类似,只是不能被实现类重写。接口静态方法直接通过接口名.方法名来调用。

接口中的默认方法、静态方法可以同时有多个。 参考文章:Java8:接口里面可以写实现方法吗【可以】 、接口可以多继承吗【可以】

抽象类的特点

抽象类中可以定义任一类型的方法,包括:

  • 抽象方法:默认是public abstract修饰,修饰符可以不写。不能有方法体。子类必须重写该方法。
  • 普通方法:
  • 静态方法:子类不可重写

抽象类中定义的成员变量

  • 无控制符限制,可以定义private类型的变量。

总结

  • 共同点:

    • 都不能被实例化
    • 都可以包含抽象方法
    • 都可以有默认实现的方法
    • 都可以定义public static的方法(可以有方法体)
  • 区别:

    • 作用不同:接口主要是对类的行为进行约束,实现了某个接口,就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
    • 继承关系:一个类只能继承一个抽象类,但是可以实现多个接口。
    • 成员变量不同:接口中的成员变量只能是public static final类型的。不能被修改而且必须有初始值。抽象类的成员变量无访问控制符限制,跟普通类的成员变量一样。
    • 方法控制符不同:接口中方法的控制符默认是public,并且不能设置为其他控制符。抽象类中的方法控制符无限制,但抽象方法不能被private修饰(因为要给子类重写的,private修饰后,子类不能访问该方法了。)
    • 静态代码块使用不同:接口中不能使用静态代码块;抽象类中可以。

子类是否可重写父类的静态方法

静态方法不存在是否重写的概念。 先看案例:

public class Fu {
    public static void show() {
         System.out.println("父类的静态方法");
    }
    public void method() {
        System.out.println("父类的一般方法");
    }
}
public class Zi extends Fu {
    public static void main(String[] args) {
        Fu fu = new Zi();
        fu.show();
        fu.method();
    }
    public static void show() {
        System.out.println("子类的静态");
    }
    public void method() {
        System.out.println("子类的一般方法");
    }
    
}

输出结果是:

父类的静态方法
子类的一般方法

所以,当父类引用指向子类对象,只会调用父类的静态方法,此行为并不具有多态性!

为什么静态方法不存在重写的说法(待进一步整理)

静态方法在类加载时就分配了内存空间,存储在方法区(元空间)。子类中如果定义了相同名称的静态方法,并不会重写,而应该是在内存中又分配了一块给子类的静态方法,所以没有重写这一说,只是单纯的名字重复了。

从执行角度看:只要方法是静态的,那么在编译时就会直接调用方法区的静态方法,即使重写了也会去方法区,并不会去方法表找方法,重写实际是无效的,从这个角度看静态方法不能被重写。

(方法表满足两个特质:其一,子类方法表中包含父类方法表中的所有方法;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。)

参考文章:

  1. 父类静态方法能否被子类重写?
  2. Java及JVM是如何识别重载、重写方法的?
  3. JVM系列之:JVM如何执行方法调用

访问控制符

四种访问控制符的修饰范围

修饰符变量方法接口
private只能修饰内部类×
default×只能修饰接口中的方法××
protected只能修饰内部类×
public

default关键字比较特殊:可以修饰变量、方法、类、接口。但是不能显示的修饰(除了接口中的default方法),也就是不能写上default关键字。如果上述四种内容,前面不加任何修饰符,则默认是default修饰的。

四种访问控制符的访问权限

修饰符当前类同一包子孙类(同一包)子孙类(不同包)其他包
private××××
default××
protected××
public

protected和default修饰符需要特殊说明:

  • 子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。其实就是不能访问父类的protected方法。

代码实践

不相干的类,且不在同一包

public class SubAbstractClass extends AbstractClassTest {
    // 前面不加任何修饰符,表明是default修饰的。如果前面加了default反而会报错。在其他包不能访问
    String getNameDefault(){
        return "Default";
    }

    // 前面不加任何修饰符,表明是default修饰的。在其他包不能访问
    String nameDefault;

    // 前面不加任何修饰符,表明是default修饰的。在其他包不能访问
    class InnerClassDefault{

    }

    // protected修饰,其他包不能访问
    protected String getNameProtected(){
        return "Public";
    }

    // protected修饰,其他包不能访问
    protected String nameProtected;

    // protected修饰,其他包不能访问
    protected class InnerClassProtected{

    }

    public String getNamePublic(){
        return "Public";
    }

    public String namePublic;


    public class InnerClassPublic{

    }

}

在其他包的类中(非子孙类),访问上述default和protected修饰的内容。可以看到,只有public修饰的可以访问。 image.png

image.png

子孙类,不在同一包

public class SubClassProtectedTest extends SubAbstractClass {

    protected String getNameProtected(){
        return "123";
    }


    public static void main(String[] args) {
        // 子类与父类不在同一包,无法直接访问父类实例的protected方法;因为编译会报错,所以注释掉
        //new SubAbstractClass().getNameProtected();
        // 子类只能访问从父类继承来的子类实例的方法
        new SubClassProtectedTest().getNameProtected();
    }
}

不相干的类,在同一包

image.png

可以看到,能访问同一包下的其他类的default、protected、public修饰的变量、方法。

深拷贝、浅拷贝、引用拷贝

浅拷贝

public class Address implements Cloneable {
    private String name;

    public Address(String name) {
        this.name = name;
    }

    public Address() {

    }

    public Address clone() {
        try {
            // 直接调用Object类的clone方法,是浅拷贝
            return (Address) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}
public class Person implements Cloneable {
    private Address address;

    public Person(Address address) {
        this.address = address;
    }

    public Person clone() {
        try {
            Person person = (Person) super.clone();
            return person;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}
public class PersonMain {
    public static void main(String[] args) {
        Person person1 = new Person(new Address("武汉"));
        Person personCopy = person1.clone();
        System.out.println(1);
    }
}

上述main方法执行结果如下:

image.png

看上述对象的地址可以发现:person1的属性,是引用类型(除了基本类型就是引用类型),浅拷贝之后,创建了一个新的对象:personCopy,但是personCopy的属性地址仍和person1的相同。

故得出结论:浅拷贝会在堆上创建一个新的对象。不过,如果原对象内部的属性是引用类型,拷贝对象和源对象共用同一个内部对象。

深拷贝

对上述Person的clone方法做一些修改(为了演示方便,这里新建了一个PersonDeepCopy类)。

clone方法中,先调用Object的clone生成一个新的person对象。然后再获取person对象的address属性,再进行clone,即可获得一个新的address对象。将其赋值到person对象中,即可实现person对象的深拷贝。

@Data
public class PersonDeepCopy implements Cloneable {
    private Address address;

    public PersonDeepCopy(Address address) {
        this.address = address;
    }

    public PersonDeepCopy clone() {
        try {
            PersonDeepCopy person = (PersonDeepCopy) super.clone();
            // person.getAddress()先获取person里的引用对象,然后再对该引用类型进行clone,就可以实现深拷贝
            person.setAddress(person.getAddress().clone());
            return person;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}
public class PersonMain {
    public static void main(String[] args) {
        Person person1 = new Person(new Address("武汉"));
        Person personShallowCopy = person1.clone();

        PersonDeepCopy person2 = new PersonDeepCopy(new Address("南京"));
        PersonDeepCopy personDeepCopy = person2.clone();
        System.out.println(1);
    }
}

上述Main方法的执行结果如下: image.png 可以看到,从person2拷贝出来的personDeepCopy对象,是深拷贝,与原有对象不共用地址

引用拷贝

两个不同的引用,指向同一个对象。

image.png

this指针

。。。

参考文章

  1. JavaGuide: Java基础常见面试题总结(中)
  2. 面试题系列第2篇:new String()创建几个对象?