MENU

类、接口和枚举

2024 年 06 月 04 日 • 访问: 303 次 • Java

修饰符

Q: 各修饰符所代表的可见性?
public: 可被所有使用
protect: 只能被自己和子类使用,或者同一个包路径
private: 只能自己使用,儿子都不行
不加修饰符即default权限: 包访问权限,和他在同一包内的类都可以访问他,包外的则都不能访问

Q: 外部类可以用private或者protect修饰吗?
A: 不能,只能用public或者包访问权限。 内部类可以。

Q: 解释以下final的作用

  • final 成员?如果是基本类型,则指值不能被改变。 如果是对象,指对象的引用不可改变,但是引用处的内容可改变。编译器会要求final成员必须初始化或者构造器里赋值,且后续不能再主动赋值。
  • final 参数?参数不可变,只能读不能修改,同上
  • final方法?方法不能被子类重写。
  • final类?该类不能被继承。

Q:final局部变量可以作为非final的参数传入吗?会编译报错吗?

public static void main(String[] args){
    final A a = new A();
    changeA(a);
}
public void changeA(A a) {
    // change A...
}

可以作为非final的参数传入,不会编译报错。

Q: 类的创建你知道有哪些方法吗?

https://www.cnblogs.com/yanchuanbin/p/14742892.html
https://cloud.tencent.com/developer/article/1497720

假设现在我们有一个Person的类

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

    public Person() {
    }

    private Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

1.new关键字创建

public class Main {
    public static void main(String[] args) throws Exception {
        Person person = new Person();
    }
}

2.Class类的newInstance

先获取其Class对象,再调用newInstance方法,这个newInstance方法调用无参的构造函数创建对象。

public class Main {
    public static void main(String[] args) throws Exception {
        Person person = Person.class.newInstance();
    }
}

3.Constructor类的newInstance

通过Class拿到指定的构造方法,再调用构造方法的newInstance方法创建对象

public class Main {
    public static void main(String[] args) throws Exception {
        // 拿默认的public的无参的构造方法
        Constructor<Person> constructor = Person.class.getConstructor();
        Person person = constructor.newInstance();

        // 包括public的和非public的所有的构造方法列表,这里拿到两个
        Constructor<Person>[] declaredConstructors = Person.class.getDeclaredConstructors();
        Constructor<Person> noArgsConstructor = declaredConstructors[0];
        Constructor<Person> haveArgsConstructor = declaredConstructors[1];
        haveArgsConstructor.setAccessible(true); // 非public的构造必须设置true才能用于创建实例
        Person person2 = haveArgsConstructor.newInstance("Alice", 20);

        // 只返回public的构造方法列表,这里拿到一个
        Constructor<Person>[] constructors = Person.class.getConstructors();
        Person person3 = constructors[0].newInstance();
    }
}

4.使用clone方法

无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去,用clone方法创建对象并不会调用任何构造函数。

使用clone方法,我们必须先实现Cloneable接口并复写Object的clone方法,因为Object的这个方法是protected的,你若不覆写,外部也调用不了

public class Person implements Cloneable {
    ...
    // 访问权限写为public,并且返回值写为person
    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Person person = new Person();
        Object clone = person.clone();

        System.out.println(person);
        System.out.println(clone);
        System.out.println(person == clone); //false
    }
}

5.反序列化

当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。

public class Person implements Serializable {
    private static final long serialVersionUID = 1L; // Intellij IDE支持自动生成UID
    ...
}

public class Main {
    public static void main(String[] args) throws Exception {
        Person person = new Person();
        byte[] bytes = SerializationUtils.serialize(person);

        // 字节数组:可以来自网络、可以来自文件(本处直接本地模拟)
        Object deserPerson = SerializationUtils.deserialize(bytes);
        System.out.println(person);
        System.out.println(deserPerson);
        System.out.println(person == deserPerson); // false
    }
}

Q: 重载和重写的区别?
A:
重载是方法名相同,参数不同。
重写是方法参数等都一致的情况下重写父类的方法。

Q: 如果子类重写了父类中的方法, 那么子类中还能调用父类中的同名方法吗?
A: 可以,super.xxx即可(C++中不可以调用父类中的同名重载方法)。

Q: 怎样能避免子类在重写父类的方法,不小心弄成了重载?
(即你想重写父类的f(int), 却不小心写成了f(int,int),导致调用f(int)时还是调用了父类的f ,怎么能避免这种失误?)
A: 加个@Override(重写)关键字即可,会自动帮你检测重写的对不对,如果不小心写成了重载,编译时编译器会有提示报错信息

Q:父类的成员变量能被重写/覆盖嘛?

class A{
    public String name = "A";
}
class B extends A{
    public String name = "B";
}
public static void main{
    A a = new B();
   System.out.println(a.name);
}

A:
输出A。
注意成员变量不具有多态性,因此你定义的是A,赋值的是B, 那么输出的依旧是A里的成员。
如果是被重写的方法的话,那会用B里的方法。

Q:内部类是啥,内部类能访问外部类的成员吗?
A:
内部类概念:

class A {
    class B{
           ...
   }
}

B就是A的内部类,B能访问A的所有成员

Q: A中有1个内部类C, A的子类B中也有1个内部类C, B中的C会覆盖A中的C吗?
A: 不会, 因为使用时是通过B.C或者A.C去调用的,存在命名空间的关系。

Q:可以在内部类中定义静态成员吗?

class A {
    class B{
        static int b;
           ...
   }
}

A:
不可以。 除非在class B前面加static变为静态类

Q: 匿名类是啥, 匿名类能访问外面的变量或者对象吗?
A: 匿名类概念:

return new A(构造参数){
   {构造器内容}
   类定义 
}

匿名类如果要用外面的对象, 外面的对象必须要定义为final。

Q: 嵌套类是啥,能访问外部类的成员吗?
A:

class A {
   static int sa;
   int a;
   static class B{}
}

B只能访问A中的静态成员sa, 而不能访问a。

接口

类是单继承,接口可以多继承

Q: 接口中如果要定义成员变量,那成员的默认修饰符是什么?
A: public static final

Q: 接口中各方法的默认修饰符是什么?
A: public abstract

Q: 接口中可以定义实现具体方法嘛?
A:
java8以上版本可以。
引入了default关键字,在接口中用default关键字修饰接口,就可以在接口中去实现这个接口了。

枚举

Q: enum可以被继承吗?
像下面这样:

enum A extend B{
...
}

A: 不可以。enum标识符本身被编译器处理过,自身就继承自Enum类,而java不支持多重继承。但支持实现接口

Q: switch(enum)时需要加default吗?
A: 可以不需要。

Q: Enum基类里实现了values()方法吗?
A: 没有实现, values方法是编译器加的。因此从List里取出的对象,是不能调用values()的。

Q:enum里的枚举的默认修饰符默认是?
A:static final

静态分配和动态分配

Q: 下面输出什么,属于什么分派?

void test() {
       Father father = new Son();          //静态分派
       print(father);
   }

   void print(Father father) {
       System.out.println("this is father");
   }

   void print(Son son) {
       System.out.println("this is son");
   }

A:
输出this is father。 属于静态分派。
静态分派概念: 编译器就能确定调用哪个方法。
这里2个print属于重载方法,通过输入参数的定义类型即立刻确定调用哪个
静态分派取决于静态类型

静态类型概念: 编译期写在java文件里能马上看到的类型
例如 A a = Factory.create(args);
那么左边的A就是静态类型, 而右边的类型取决于运行期,是不确定的。

Q: 涉及如下各种不同数据类型的静态分派如何应对?

public class Overload {
  private static void sayHello(char arg){
    System.out.println("hello char");
  }
  private static void sayHello(Object arg){
    System.out.println("hello Object");
  }
  private static void sayHello(int arg){
    System.out.println("hello int");
  }
  private static void sayHello(long arg){
    System.out.println("hello long");
  }
  // 测试代码
  public static void main(String[] args) {
    sayHello('a');
  }
}

输出什么?
A:
输出 hello char
因为‘a’是一个char类型数据(即静态类型是char),所以会选择参数类型为char的重载方法。
若注释掉sayHello(char arg)方法,那么会输出 hello int
因为‘a’除了可代表字符串,还可代表数字97。因此当没有最合适的sayHello(char arg)方式进行重载时,会选择第二合适(第二优先级)的方法重载,即sayHello(int arg)

总结:当没有最合适的方法进行重载时,会选优先级第二高的的方法进行重载,如此类推。
优先级顺序为:char>int>long>float>double>Character(自动装箱)>Serializable(接口,从下往上)>Object(父类1,父类2,从下往上)>…
其中…为变长参数,将其视为一个数组元素。变长参数的重载优先级最低。
因为 char 转型到 byte 或 short 的过程是不安全的,所以不会选择参数类型为byte 或 short的方法进行重载,故优先级列表里也没有。

上面可以看到,重载时选择方法的优先级顺序是基本类型->高精度类型->包装类->接口(从下往上)->父类(从下往上)->可变参数

Q: 下面输出什么,属于什么分派:

void test() {
     Father father = new Son();       
     father.name();    
 }
 
 class Son extends Father {
     void name(){
         System.out.println("son");
     }
 }
 
 class Father {
     void name(){
         System.out.println("father");
     }
 }

A:
输出son,属于动态分派。运行的时候根据所指向的具体对象才确定调用哪个方法

Q:静态分派属于单分派还是多分派?动态分派属于单分派还是多分派?
A:
静态分派是多分派。
动态分派是单分派。
多分派概念: 分派时即要考虑调用者的类型,也要考虑参数类型。
而单分派只考虑调用者的类型。

Q:类方法在class文件中是什么样的? 是符号引用还是直接引用?
A:
class文件中, 所定义的方法 都只是符号引用,即只是个符号,知道方法名字, 但是不知道方法的实际指令运行地址
符号引用如下,不过我这展示的时class_info即类的符号引用, 实际上还会有method_info即方法的引用:

然后方法在class文件中时这样存放的, 先是一个method_count数量,接着再存储方法。

此时是不知道方法的指令地址的。 除非从符号引用转为直接引用。

Q:什么时候方法的符号引用会转为实际方法区中的直接引用?
A:
类加载的解析阶段会把满足「编译期可知,运行期不可变」的方法的符号引用替换为指向方法区的直接引用,不会延迟到运行时再去完成。

构造、私有、静态、final修饰

上面这4类方法在类加载时都会被识别出来,并转成 指向方法区的直接引用(即能知道了指令地址了,而不是字节码的符号)

Q:动态分派(即多态), 虚拟机里是怎么确定调用哪个方法的?
如下, 他怎么确定调用的是Son实现的do, 还是father实现的do?

int a = 1;
Father f = new Son()
f.do(a);

A:
首先,通过静态分派, 他知道一定选用的是f(int a) 这个方法,但是他不知道选用哪个类的do(int a)方法。
而你执行f.do(a)时, 操作数栈上会存放一个对象引用

那么执行f方法的虚拟机指令就会通过这个对象引用,找到他的实际类型的class

他会在这个实际类中查找是否有实现这个方法,具体看class文件中有没有这个方法的定义

如果没有找到,他就去父类找,父类的关系class文件中就可以知道

如果父类没有,就接着往上找,直到找到实现了的。

最后编辑于: 2024 年 10 月 24 日
返回文章列表 打赏
本页链接的二维码
打赏二维码