LOADING

加载过慢请开启缓存 浏览器默认开启

Java期末拾遗-上半

哎哟,又到期末了,java有一些细碎知识还不会,
故在此收集并记录,我在期末拾来的java知识。

1.传值和传址,Java与C
传值和传址
传值和传值的区别是什么?
假如现在你电脑桌面有个word文档,传值就是你把这文档创建了一个副本,
然后你是原封不动的把这个文档提交了,或是把它修改了,都跟你最初的文档一点关系没有。
然后传址就是你把原文件改了。
对于C/C++,我们可以真正传递地址(指针),通过指针修改原始变量。
而对于Java,Jesus Crime!
我们无法修改基本变量的值!
Java 中所有参数传递本质上是值传递,但分两种情况:
对于基本类型(int, double 等),传的是值的副本,对值无法改变

public class Main {
    public static void main(String[] args) {
        int x = 10;
        change(x);
        System.out.println(x);  // 输出仍是 10
    }

    static void change(int val) {
        val = 20;  // 改的是 val 的副本
    }
}

x 的值没有变,因为change()只是把 x 的副本传进去了,这个change()只是自欺欺人罢了。
但是对于对象和数组,虽然值传递的“祖宗章程”没变,但是情况有变。
如果是对象或是数组,传的是“引用的副本”,即拷贝了一份引用数据的地址,通过改变引用数据的地址可以改对象内部。

class Person {
    String name;
}

public class Demo {
    public static void change(Person p) {
        p.name = "Alice";   // 改变对象内容 ✅
        p = new Person();   // 改变引用本身 ❌(不会影响原来的 p)
        p.name = "Sunshine";     // 改变的是新对象
    }

    public static void main(String[] args) {
        Person p1 = new Person();
        p1.name = "Tom";
        change(p1);
        System.out.println(p1.name); // 输出是 Alice
    }
}

引用数据类型存储的是“对象的地址”(引用)。
在这个例子里面,name原来引用Tom的地址,使得p1的name对应为Tom。
然后再change()函数里面,将引用的数据改为“Alice”,将新的ALice的地址给name引用,从而改变了对象的内容。
但是下一步尝试将新对象赋给p,实际只是创建了一个p的副本,让这个副本指向了一个新的对象,
原来p不变,p1自然不变,还是掩耳盗铃。

2.重写euquals方法
Q1:“为什么不直接用等号 == 对比两个对象,而要写 equals() 这么麻烦?”
A:== 比较的是两个对象的内存地址是否相同(引用是否一致),即判断是否是同一个对象。
如果你不重写 equals() 方法,默认行为等同于 ==(Object类默认的实现)。
重写equals方法后,比较的是两个对象的逻辑内容是否相同(可以自定义什么算逻辑内容相同),即判断是否是“内容上相等”。
举个生活中的例子,== 相当于判断:这两张身份证是不是“完全同一张卡片”?(内存地址)
equals() 问题:这两张身份证上是不是“属于同一个人”?(内容逻辑)
Q2:正确重写equals方法示例?

public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Product product = (Product) obj;
        return initialCode.equals(product.initialCode);
    }

(当 if 语句后面只有一条语句时,可以省略大括号 {}。执行语句仅为后面的那一句,但这只是语法允许,不代表是最佳实践。)
Q3:为什么要强制转化成product类?
Object 是所有类的父类,而 equals(Object obj) 方法的参数类型是 Object。
这句代码是把一个通用的 Object 类型,强制转换成你自己定义的具体类型 Product,
否则你就无法访问 Product 类中的字段或方法,比如下一句的比较initialCode,必须是访问 Product类才可以。
所以你必须告诉编译器:”我知道这个对象其实是 Product 类型”,于是要强制转换。
由于在前面已经做了类型检查:

if (obj == null || getClass() != obj.getClass()) return false;

所以之后进行强制类型转换是安全的。

3.父类构造器与super()
Q1:父类可以没有任何构造器吗?
A:父类必须有构造器,但可以不显式写(你自己手敲)
如果你没有写任何构造器,Java 会自动帮你加一个无参构造器(默认构造器)。

class Parent {
    // 没写构造器,Java 自动添加:Parent() {}
}

一旦你写了任意一个构造器(哪怕是带参数的),Java 就不会自动添加默认构造器了。
Q2:子类怎么调用构造器?
对于子类、父类构造器而言,它只能够被调用,而不能被继承。
子类会默认调用父类的构造器,但是如果没有默认的父类构造器(即构造器是有参的),
子类必须要显示的指定父类的构造器,即使用super(),
而且必须是在子类构造器中做的第一件事(第一行代码)。
如:

super(name);  // 调用父类 Person 的构造方法 Person(String name)

父类构造器参数需要什么类型,你在括号里就填什么类型。
与此同时,如果父类有多个构造器(不同构造器参数不同)
可以重载多个子类构造器,分别调用不同的 super(…),
在括号里面填入不同父类构造器对应的数据类型即可。
补充:super(“Alice”); 和 super(name); 都是调用父类构造器的方式。
super(“Alice”); 是直接传字面量,和子类有没有参数无关。
super(name); 是传递子类构造器的参数,将子类构造器中的 name 变量传递给父类构造器。子类构造器传进什么,就传给父类,让父类帮忙构造。

4.抽象类和抽象方法
Q1:什么样的类、方法是“抽象”的?
我们把没有方法主体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类,例如:

abstract class Animal //包含抽象方法,故为抽象类
 {
    abstract void makeSound(); // 所有动物都必须有叫声(抽象方法)
}

Q2:既然没有方法主体,抽象类和抽象方法有什么用?
A:有的兄弟,有的。使用抽象类和抽象方法的主要目的是建立一个“通用模板”或“规范”,
它强制子类实现特定的方法行为,从而达到代码的可扩展性、可维护性和一致性。
继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父类的抽象方法,否则,
从最初的父类到最终的子类都不能创建对象,失去意义。所以抽象方法抽象类给子类指派了重写所有抽象方法的任务。
就像愚公临死前给子孙说了:“一定要移走门口的大山!”,子孙就必须实现这个任务(按中国传统孝道文化来说),
父亲没实现,孙子和重孙也得努力实现,直到所有抽象方法实现,当然,怎么实现是子类的事情,父类只需要指派任务就行了。
此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法,例如:

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof! Woof!");
    }
}

注意事项:
一.抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。
理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
二.抽象类中,可以有构造方法,是供子类(已经重写所有抽象方法)创建对象时,初始化父类成员使用的。
三.抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
抽象类中可以包含非抽象方法,写一些公共逻辑,这样子类可以复用,例如:

abstract class Animal {
    abstract void makeSound();
    
    public void breathe() {
        System.out.println("Breathing...");
    }
}

每种动物都能呼吸,但发声不一样,我们把通用的breathe()及其方法主体写在抽象类里,作为公共逻辑,特有的 makeSound() 让子类各自实现。

5.接口
Q1:接口与抽象类的区别?
A:如果抽象类比喻为魔园里面的魔法少女的话,虽然关键部分方法被抽象了,但还保留部分人的特征,
接口就是已经飞升到宇宙的圆神,变成圆环之理,成为指导所有引用该接口的类的规则了(哭,可爱的小圆和晓美焰)
接口没有构造函数,只能有常量(public static final),但是相比类可以实现多继承,即一个子类实现多个接口。
接口,是java语言中一种引用类型,是方法的集合,接口的内部主要就是封装了方法,
包含抽象方法、默认方法和静态方法,私有方法(JDK 9)。
接口的定义,与定义类相似,但是使用interface关键字。接口不能创建对象,但是可以被实现(implements,类似于被继承)。

 public interface InterFaceName {
 public abstract void method();
  public default void method() {
  System.out.println("This is a default method");
  // 默认方法需执行语句
}
 public static void method2() {
  System.out.println("This is a static method");
  // 抽象方法需执行语句
  }
 }

与抽象方法的子类类似,一个实现接口的类,需要实现接口中的所有的抽象方法,
然后创建该类对象,就可以调用方法了,否则它必须是一个抽象类。
非抽象子类实现接口:1.必须重写接口所有的抽象方法。
2.继承了接口的默认方法,既可以直接调用,也可重写。

class类名implements接口名{
 // 重写接口中抽象方法【必须】
// 重写接口中默认方法【可选】
}

Q2:抽象方法、默认方法和静态方法的区别?
A:抽象方法必须没有方法体,必须被重写。
默认方法类必须有方法体,可以不重写直接调用,也可以重写(注意default不能省略)。
静态方法也必须有方法体,只能用接口名调用,不可以被重写,不可以通过实现类的类名或者实现类的对象调用,例如:

interface Tool {
    static void info() {
        System.out.println("Interface static method");
    }
}

class MyTool implements Tool {}

public class Main {
    public static void main(String[] args) {
        MyTool t = new MyTool();
        //t.info();         // ❌ 编译错误:不能通过对象调用接口的静态方法

        Tool.info();          // ✅ 正确:接口名调用
    }
}