KaiSpace
tech

Java OOP难点详解

CS2030S笔记补充

继承

1. Java中field或method字段冲突:

首先Java中只可能有一个extend和多个interface

  1. 如果字段冲突,则可以通过interfaceName.FIELDclassName.FIELD来分别调用,如果没有指明前缀,则优先调用className,不会调用interface

  2. 如果method冲突,优先调用父类。如果是interface中的abstract class冲突,则只需一次定义即可满足所有。如果interface中的默认方法冲突,那么必须要进行Override,否则编译器将会不知道究竟调用哪一个。

    public class MyClass implements InterfaceA, InterfaceB {
        @Override
        public void conflictedMethod() {
            // 选择使用 InterfaceA 的默认实现
            InterfaceA.super.conflictedMethod();
        }
    }
    
    

2.编译器如何确定调用的method

首先在编译的时候,java只关注CTT (compile time type),并且通过CTT对应method signature查找最符合的method(最符合:范围最小,最子类),将这个函数的signature(指定,不存在模糊)存下来。等到运行后再用存下来的signature与RTT对应,找到RTT下完全一致的method。

例子(chatgpt生成,确认正确):

class Animal {
    public void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    // 重写(Override)父类的speak方法
    @Override
    public void speak() {
        System.out.println("Dog barks");
    }
    
    // 重载(Overload)方法,根据参数不同
    public void speak(String mood) {
        if ("happy".equals(mood)) {
            System.out.println("Dog wags tail and barks happily");
        } else {
            System.out.println("Dog barks in a different tone");
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 编译时类型是 Animal,运行时类型是 Dog
        
        // 调用无参的speak方法
        // 编译时根据Animal类型确定了方法签名为 speak()
        // 运行时,根据Dog中的重写,调用 Dog.speak()
        animal.speak();  // 输出: Dog barks
        
        // 如果尝试调用重载方法 speak(String),编译器会根据变量的编译时类型Animal来查找匹配的方法
        // 由于Animal中没有speak(String)方法,下面代码会在编译期报错:
        // animal.speak("happy");
        
        // 要调用重载方法,就必须使用Dog类型或者进行强制类型转换:
        ((Dog) animal).speak("happy");  // 输出: Dog wags tail and barks happily
    }
}

解释

  • 编译时决议:
    当编译器遇到 animal.speak() 时,会根据 animal 的编译时类型 Animal 来寻找 speak() 方法,并确定调用的签名是 speak()(无参数)。因此,尽管运行时对象是 Dog,编译器已经锁定了无参的 speak() 方法。

  • 运行时绑定:
    当运行到 animal.speak() 时,JVM根据 animal 的实际类型 Dog,找到 Dog 中重写的 speak() 方法,并调用它,输出 "Dog barks"。

  • 重载的情况:
    如果调用 speak("happy"),编译器会在 Animal 类中查找相应的方法签名,但由于 Animal 没有 speak(String),因此会导致编译错误。只有在变量的编译时类型为 Dog 或通过类型转换后,才能调用 speak(String)

  • 变体

    如果dog中不存在speak()方法则在父类中寻找这个方法并调用。

3. toString隐式调用

  • 在使用System.out.println(obj)时,会自动输出obj.toString

  • 在使用obj + "string"时,会自动调用obj.toString

4. getter,setter

在java中尽量少使用(谨慎使用,不是不用)getter和setter (Tell, Don't Ask),如果需要访问某个class中的instance的method,则可以在class中写一个method调用instance的method。

例子:

class Engine {
    public void start() {
        System.out.println("Engine starting...");
    }
}

class Car {
    // 私有的内部实例,不对外暴露
    private Engine engine;

    public Car() {
        engine = new Engine();
    }

    // 提供一个行为方法,内部调用 engine 的 start 方法
    public void startEngine() {
        engine.start();
    }
}

public class Test {
    public static void main(String[] args) {
        Car car = new Car();
        // 直接告诉 car 启动引擎,而不是先获取 engine 再启动
        car.startEngine();
    }
}

5. Exception处理

  1. checked exception (extends Exception):

    如果在某个函数中出现了这个exception,在当前函数必须被catch或throw,如果使用throw,则必须在调用链的某个地方catch。否则编译器会报错。如果出现exception,程序会跳过后面所有操作,直到回到catch中。

    no exception

    throw exception

    这是java的编译器设计逻辑,为了帮助程序员提早预知一些可预见错误的发生并且给出解决。但是这个设计也被部分程序员批评。

    <details> <summary>为什么checked exception的设计被部分程序员批评?</summary>

    虽然checked exception的设计初衷是强制开发者考虑异常情况,提高程序的健壮性,但它也遭到了不少批评,主要原因包括:

    • 代码冗长和噪音
      每个可能抛出受检异常的方法都必须在调用者中进行捕获或继续抛出,往往导致大量的try-catch块,增加了代码的冗长和复杂性。

    • 异常处理滥用
      有时候开发者不得不捕获异常,但实际上没有办法合理地处理它们,最终只能捕获后重新抛出或简单打印日志,这样的处理往往无法真正解决问题。

    • 破坏抽象和封装
      某些底层API可能会抛出细节丰富的异常,但这些细节对于调用者来说并不重要。强制调用者处理这些异常会使得上层代码被迫暴露底层实现的细节,破坏了封装性。

    • 灵活性不足
      在某些设计中,开发者更倾向于使用unchecked exception(运行时异常),因为这些异常不需要在方法签名中声明,能让代码看起来更简洁,并允许开发者在需要时集中处理异常(例如通过全局异常处理器)。

    </details>
  2. unchecked exception (extends RuntimeException)

    这类异常往往是程序员编程错误,很难预知,在编译时不要求捕获。一般对它的处理为不捕获或局部捕获。

    1. 不捕获:尽量让异常暴露出来,便于查找错误根源。

    2. 局部捕获:在特定场景下可以捕获运行时异常,比如对用户输入进行校验或在某些临界业务中使用 try-catch 块记录详细错误信息,防止程序崩溃。

      public class Calculator {
          // 除法方法可能会抛出ArithmeticException(例如除以0)
          public int divide(int a, int b) {
              return a / b;
          }
          
          public static void main(String[] args) {
              Calculator calc = new Calculator();
              try {
                  int result = calc.divide(10, 0);
                  System.out.println(result);
              } catch (ArithmeticException e) {
                  // 在这里捕获并处理运行时异常,防止程序崩溃
                  System.err.println("运算错误:" + e.getMessage());
              }
          }
      }
      
      
    <details> <summary>自定义exception(由chatgpt生成,未检验正确性)</summary>

    一般来说,许多程序员在自定义异常时更倾向于使用 unchecked exception(继承自 RuntimeException)。主要原因包括:

    • 代码简洁性
      unchecked exception 不要求在方法签名中声明或在调用处捕获,从而避免大量冗余的 try-catch 代码,使得 API 更加简洁。

    • 编程错误和不可预见性
      unchecked exception 通常用于表示程序中的逻辑错误或不可能发生的情况,而这类错误往往应当在开发阶段就解决,而不是在运行时通过捕获来处理。

    • 业务逻辑的灵活处理
      对于很多业务场景,往往希望让全局或上层统一处理异常,而不是在每个调用点都显式捕获 checked exception。

    但是,也有例外情况:
    如果异常代表一种可恢复的情况,并且设计者希望调用者显式处理(比如文件不存在、网络超时等情况),那么使用 checked exception 可能更合适。

    总结:
    多数情况下,自定义异常时会选择 unchecked exception,因为它们减少了代码噪音和强制捕获的负担;但如果异常情况确实需要调用者主动处理以实现恢复逻辑,那么 checked exception 也是合适的选择。

    </details>

type & generic

Java类型机制 & 不用generic可能导致的错误

Java 语言在编译时使用编译时类型(Compile-Time Type,CTT)来检查代码正确性,而在运行时则依据运行时类型(Run-Time Type,RTT)进行动态绑定。这样的设计既保证了编译时的类型安全,又允许在运行时通过多态实现灵活调用。

但是这种CTT和RTT结合的情况很容易出现由于typecast错误而导致的各种runtime exception,下面我将一一列举(chatgpt生成,检验正确)。

1. 类型安全性降低

后果:
使用类型转换容易绕过编译器的类型检查,可能在运行时抛出 ClassCastException,导致程序崩溃或行为异常。

代码示例:

import java.util.ArrayList;
import java.util.List;

public class TypeSafetyExample {
    public static void main(String[] args) {
        // 未使用泛型的容器
        List list = new ArrayList();
        list.add("Hello");
        list.add(123);  // 无法在编译期发现错误

        for (Object obj : list) {
            // 强制类型转换,假设全部为String
            String str = (String) obj; // 当遇到Integer时,会抛出异常
            System.out.println(str);
        }
    }
}

在上述代码中,当尝试将 Integer 转换为 String 时,会抛出 ClassCastException

泛型解决方案:
使用泛型时,编译器在编译期就能检查容器中元素的类型,避免错误的类型转换。

import java.util.ArrayList;
import java.util.List;

public class GenericExample {
    public static void main(String[] args) {
        // 使用泛型,指定容器只能存放String类型
        List<String> list = new ArrayList<>();
        list.add("Hello");
        // list.add(123);  // 编译期错误

        for (String str : list) {
            System.out.println(str);
        }
    }
}

泛型使得代码在编译期就能捕捉类型不匹配的错误,保证了类型安全。


2. 可维护性和可读性下降

后果:
没有明确的类型信息,代码阅读时需要额外理解类型转换逻辑,降低代码的可读性和维护性,特别是当代码量较大时,容易引入隐患。

代码示例:

import java.util.ArrayList;
import java.util.List;

public class MaintainabilityExample {
    public static void main(String[] args) {
        // 未使用泛型,代码可读性较差
        List data = new ArrayList();
        data.add("Test");
        data.add(100);

        for (Object o : data) {
            // 需要明确判断类型后转换
            if (o instanceof String) {
                String s = (String) o;
                System.out.println("String: " + s);
            } else if (o instanceof Integer) {
                Integer i = (Integer) o;
                System.out.println("Integer: " + i);
            }
        }
    }
}

这种代码需要开发者手动管理和判断每个元素的类型,逻辑较为混乱,难以维护。

泛型解决方案:
泛型明确了容器中数据的类型,使得代码逻辑更直观,减少了类型判断的分支和错误可能。

import java.util.ArrayList;
import java.util.List;

public class GenericMaintainability {
    public static void main(String[] args) {
        // 使用泛型,明确容器中只允许存放String类型
        List<String> data = new ArrayList<>();
        data.add("Test");
        // data.add(100); // 编译错误

        // 无需额外判断,直接处理
        for (String s : data) {
            System.out.println("String: " + s);
        }
    }
}

代码逻辑清晰、易于维护,降低了错误风险。


3. 重用性和扩展性受限

后果:
不使用泛型时,容器或方法往往只适用于特定数据类型。如果需要支持多种类型,就需要重复编写代码或引入大量转换逻辑,降低了代码的复用性与扩展性。

代码示例:

public class ReusabilityExample {
    // 用于处理String的转换方法
    public static String processString(Object obj) {
        return (String) obj;
    }

    // 用于处理Integer的转换方法
    public static Integer processInteger(Object obj) {
        return (Integer) obj;
    }

    public static void main(String[] args) {
        String result1 = processString("Hello");
        Integer result2 = processInteger(100);
        System.out.println(result1 + ", " + result2);
    }
}

此处为了支持不同类型,需要分别实现多个方法,代码重复性高且扩展时需要修改多个地方。

泛型解决方案:
泛型使得可以编写通用的方法或类,能在不重复代码的前提下支持多种数据类型。

public class GenericReusabilityExample {
    // 使用泛型定义一个通用方法
    public static <T> T process(T obj) {
        return obj;
    }

    public static void main(String[] args) {
        String result1 = process("Hello");
        Integer result2 = process(100);
        System.out.println(result1 + ", " + result2);
    }
}

通过泛型,一个方法即可适用于任何类型,提高了代码的复用性和扩展性。


4. 接口契约失效

后果:
在没有泛型的情况下,方法接口不能明确规定参数和返回值的类型,调用者和实现者之间的契约不明确,容易导致错误使用或误解接口设计。

代码示例:

// 非泛型接口:返回值为 Object,契约不明确
interface DataProvider {
    Object getData();
}

class StringDataProvider implements DataProvider {
    @Override
    public Object getData() {
        return "Hello";
    }
}

class IntegerDataProvider implements DataProvider {
    @Override
    public Object getData() {
        return 123;
    }
}

public class InterfaceContractExample {
    public static void main(String[] args) {
        // 正确使用:期望 String 的提供者
        DataProvider provider = new StringDataProvider();
        // 调用者需要进行强制类型转换,编译期无法校验类型正确性
        String data = (String) provider.getData();
        System.out.println("StringDataProvider returns: " + data);

        // 错误使用:错误地将 Integer 提供者当作 String 使用
        provider = new IntegerDataProvider();
        // 运行时将抛出 ClassCastException
        String wrongData = (String) provider.getData();  
        System.out.println("IntegerDataProvider returns: " + wrongData);
    }
}

若将 getData() 修改返回其他类型,调用者就会因为错误的类型转换而出现问题,接口契约隐含且脆弱。

泛型解决方案:
使用泛型,可以在接口中明确规定参数与返回值类型,形成明确的契约,方便调用者正确使用。

// 定义一个泛型接口
interface DataProvider<T> {
    T getData();
}

class StringDataProvider implements DataProvider<String> {
    public String getData() {
        return "Data";
    }
}

public class GenericInterfaceContractExample {
    public static void main(String[] args) {
        DataProvider<String> provider = new StringDataProvider();
        // 无需类型转换,直接获得String类型数据
        String data = provider.getData();
        System.out.println(data);
    }
}

通过泛型,接口定义和实现者都明确了数据类型,增强了代码的健壮性和易用性。

变异

协变与逆变:

  • 协变(Covariance): 允许子类型的集合赋值给父类型的引用。在 Java 中,数组是协变的,例如:Dog[] 可以被赋值给 Animal[]
  • 逆变(Contravariance): 允许父类型用于要求子类型的场景,但在 Java 泛型中不直接支持逆变。

Java中数组与泛型的对比:

  • 数组协变的弊端: 虽然允许将 Dog[] 赋值给 Animal[],但随后如果向数组中存入一个 Cat 对象(假设 Cat 同样继承自 Animal),则会在运行时导致 ArrayStoreException
  • 泛型不变性: 为了避免这类运行时错误,Java 的泛型默认是不变的,即 List<Dog> 不能直接赋值给 List<Animal>

设计动机:
保持类型安全,防止在运行时因类型不匹配而出现难以调试的错误。

generic(泛型)

1. 泛型基本概念

1.1. 语法与基本用途

  • 泛型类和接口
    泛型类允许我们将数据类型参数化,从而在编译时进行类型检查。例如:

    public class Box<T> {
        private T content;
        
        public Box(T content) {
            this.content = content;
        }
        
        public T getContent() {
            return content;
        }
        
        public void setContent(T content) {
            this.content = content;
        }
    }
    

    使用时可以指定具体类型:

    Box<String> stringBox = new Box<>("Hello");
    String content = stringBox.getContent();
    
  • 泛型方法
    方法也可以被参数化,这样就不需要在类级别声明泛型参数。例如:

    public class Util {
        public static <T> void printArray(T[] array) {
            for (T element : array) {
                System.out.print(element + " ");
            }
            System.out.println();
        }
    }
    

    这种方式使得方法在不同上下文中灵活适用,而无需进行重复代码编写。

1.2. 设计动机

  • 编译时类型检查
    泛型使得编译器在编译阶段就能发现类型不匹配的错误,避免在运行时因为类型转换错误(例如 ClassCastException)而导致程序崩溃。

  • 消除强制类型转换
    以前在使用集合时,我们需要频繁进行类型转换,例如:

    List list = new ArrayList();
    list.add("Hello");
    String s = (String) list.get(0);
    

    使用泛型后可以直接获得正确类型:

    List<String> list = new ArrayList<>();
    list.add("Hello");
    String s = list.get(0);
    

2. 类型擦除(Type Erasure)

泛型在编译后的字节码中并不会保留完整的类型参数信息,而是通过“擦除”(Erasure)机制将泛型参数替换为其限定类型(若无指定上界,则为 Object)。这带来了一些重要影响:

2.1. 编译时检查与运行时类型

  • 编译时
    编译器利用泛型类型参数进行严格的类型检查,确保在源代码层面类型正确。例如,List<String> 只能存放 String 类型的对象,编译器会拒绝加入其他类型。
  • 运行时
    由于类型擦除,运行时所有的泛型信息都会被擦除,例如 List<String>List<Integer> 都变成了raw type List。因此,运行时无法直接判断集合中存放的具体泛型参数。

2.2. 边界情况与限制

  • 实例化泛型数组的限制
    由于运行时类型擦除,无法创建一个确切类型的泛型数组。例如,以下代码将报错:

    // 错误:无法直接创建泛型数组
    T[] array = new T[10];
    

    通常的解决方案是使用 Object 数组并进行强制转换,但这也会产生 unchecked 警告:

    @SuppressWarnings("unchecked")
    T[] array = (T[]) new Object[10];
    

    对于更复杂的generic:

    @SuppressWarnings("unchecked")
    NumberProcessor<Integer>[] arr = (NumberProcessor<Integer>[]) new NumberProcessor[10];
    arr[0] = new NumberProcessor<Integer>(3);
    
  • 无法使用 instanceof 检查泛型参数
    由于泛型信息在运行时不存在,不能编写如下代码:

    if (obj instanceof List<String>) { // 错误
        // 无法判断泛型具体类型
    }
    

    只能检查原始类型,如 if (obj instanceof List)

  • 方法重载冲突
    泛型方法在经过类型擦除后,可能会出现签名冲突。例如,两个方法的签名仅在泛型参数上有区别,擦除后就会产生同样的签名,导致编译错误。一般通过在一个方法中加入辅助函数处理,例如:

    public class MyClass<U, T> {
        public void process(U u) {
            System.out.println("Processing U: " + u);
        }
    
        // 通过增加额外参数使签名不同
        public void process(T t, int dummy) {
            System.out.println("Processing T: " + t);
        }
        
        // 为了方便调用,也可以提供一个不带 dummy 参数的重载,
        // 但内部调用上面的带 dummy 的方法
        public void processT(T t) {
            process(t, 0);
        }
    }
    
    

​ 或者尽量只提供一个method。

  • 桥接方法(Bridge Methods)
    当子类重写父类的泛型方法时,为了保持多态性,编译器会自动生成桥接方法。这些方法用于确保子类在运行时能够正确覆盖父类的方法,同时兼容类型擦除带来的差异。

    // 定义一个泛型父类,方法返回类型为 T
    class Parent<T> {
        public T getValue() {
            return null;
        }
    }
    
    // 子类将泛型参数指定为 String,并覆盖 getValue 方法,返回 String 类型
    class Child extends Parent<String> {
        @Override
        public String getValue() {
            return "Hello";
        }
    }
    
    public class BridgeMethodDemo {
        public static void main(String[] args) {
            Parent<String> p = new Child();
            // 调用 getValue 方法,编译器会确保返回的是 Object 类型(父类签名),而实际返回的是 String
            Object result = p.getValue();
            System.out.println(result);
        }
    }
    
    

    对于上面的代码,编译期间,由于类型擦除,实际上保存的signature为public Object getValue(),在运行时,实际type为Child,这时应该使用Child的method,但是由于Child里面没有保存的signature,所以无法执行。这种情况下,Java 编译器会在编译 Child 类时自动生成一个桥接方法。桥接方法的作用是确保子类既拥有与父类一致的方法签名,又能调用子类中实际实现的覆盖方法。编译器生成的桥接方法大致相当于下面这种伪代码:

    // 这是编译器自动生成的桥接方法,隐藏在 Child 类中
    public Object getValue() {
        // 调用子类真正实现的方法,并将返回值作为 Object 返回
        return this.getValue();
    }
    
    

    这样就可以在runtime进行调用了。


3.unchecked warnings

由于type erasure导致runtime无法检查类型转换而抛出的warning。

出现的情况例如强制类型转换T[] array = (T[]) new Object[10];List<Integer> intList = (List<Integer>) obj;,不指定generic类型而直接用raw type。

抑制警告的使用
在确认转换是安全的前提下,可以使用 @SuppressWarnings("unchecked") 来抑制警告,但这必须谨慎使用,以防掩盖真正的类型安全问题。在使用时,按照编程规范必须写上为什么可以suppress warnings,如“写了一个method只允许将T类型的object放入T[]数组”等。


4. 通配符(Wildcards)

通配符提供了更灵活的泛型类型匹配方式,? extends A的意思并非A的某一个特定子类,而是同时为A的所有子类。wildcards主要有以下几种形式:

4.1. 无界通配符:?

  • 意义
    表示未知的类型,当不关心具体类型时使用。例如,List<?> 可以表示任意类型的 List。
    请注意,List<?> 完全等同于List<? extends Object>
  • 使用场景
    通常用于只需要读取数据而不需要写入数据的场合,确保类型安全的同时保持泛型灵活性。

4.2. 上界通配符:? extends T

  • 意义
    表示泛型参数可以是 T 或 T 的子类,通常用于只读数据的场景。
    • 例如,List<? extends Number> 表示这个列表的元素至少是 Number 类型,可以是 Integer、Double 等。
  • 限制
    • 读取安全:可以安全地读取出数据并赋值给 Number 类型。
    • 写入受限:不能向该集合写入任何非 null 的值,因为编译器无法判断具体类型。例如,List<? extends Number> 不能添加 Integer 或 Double,因为可能会破坏类型约束。

4.3. 下界通配符:? super T

  • 意义
    表示泛型参数可以是 T 或 T 的父类,通常用于只写数据的场景。
    • 例如,List<? super Integer> 表示这个列表可以接受 Integer 或 Integer 的子类对象写入。
  • 限制
    • 写入安全:可以安全地添加 Integer 类型的对象。
    • 读取受限:读取时只能确保返回的对象是 Object 类型,因为具体类型可能是 Integer、Number 或 Object。

4.4. 边界情况与注意事项

  • PECS原则
    常用原则:“Producer Extends, Consumer Super”(生产者使用 extends,消费者使用 super)。即:如果集合只用于生产(读取),使用 ? extends T;如果只用于消费(写入),使用 ? super T这与只有子类能赋值给父类的原则相同。

  • 混合读写的情况
    当集合既需要读又需要写时,通常不适用通配符,而应明确指定类型参数。

  • new 不能加 wildcard

    List<? extends Fruit> list = new ArrayList<? extends Fruit>(); 
    

    这个代码是错的,必须要明确new ArrayList<Fruit>

4.5. 两层wildcard

  1. List<? extends A<Fruit> >

    List中的每一个元素必须是A<Fruit>的子类型。如果B extends A<Fruit>那么B是可以存在于list中的。

  2. List<A<? extends Fruit> >

    List中的每一个元素必须是A<? extends Fruit>,例如A<Apple>, A<Fruit>, A<Orange>

  3. List<? extends A<? extends Fruit> >

    List中的每一个元素必须是A<? extends Fruit>的子类型,如果B extends A<Orange>,那么B是可以存在于list中的。


5. 类型推断(Type Inference)

类型推断让泛型的使用更加简洁,编译器可以根据上下文自动推断出泛型参数的具体类型。函数推断的依据调用时method signature中传入argument的类型和返回值的上下文,并不会查看方法内部的实现细节。

5.1. 钻石操作符

  • 示例

    List<String> list = new ArrayList<>(); // 编译器推断 new ArrayList<String>()
    

    这样不仅减少了冗余代码,也降低了错误的可能性。

5.2. 泛型方法的类型推断

  • 方法调用中的推断
    当调用泛型方法时,编译器可以根据传入参数和返回值预期类型推断泛型参数。例如:

    public static <T> T pick(T a, T b) {
        return a;
    }
    
    String s = pick("apple", "orange"); // 编译器推断 T 为 String
    

5.3. 边界情况

  • 复杂场景下的推断失败
    在某些嵌套或复杂泛型场景中,编译器可能无法正确推断出类型参数,此时可能需要显式指定。例如:

    // 复杂嵌套类型时可能需要指定类型参数
    Map<String, List<Integer>> map = new HashMap<String, List<Integer>>();
    

CS2030S EX4中遇到的问题解答

/**
 * A generic box storing an item.
 * CS2030S Exercise 4
 * AY23/24 Semester 2
 *
 * @author zhang kai (Lab 16A)
 */

class Box<T> {
  private final T content;
  private static final Box<?> EMPTY_BOX = new Box<> (null); //cannot use ? when instantiating a generic class. This is in fact doing a type inference.
	//private static <T> final Box<? extends T> EMPTY_BOX;  
    //this is wrong, because static field cannot use generic parameter. 
    //Otherwise, since this can be accessed without instantiation, the type will lead to confusion.

  private Box (T content) {
    this.content = content;
  }

  @Override
  public boolean equals (Object obj) { 
      //I wrote:
      //return this.content.equals(obj.content)
      //but this is wrong. Because it will not recursively call this specific method. 
      //In fact, the CTT of content are Object, so Object.equals(Object) will be invoked instead. 
      //Remember that only when the parent has generic, and the child class pins down the generic will bridge methods be generated. 
      //In this case, no bridge methods are generated.
    if(this == obj) { //this handles emptybox == emptybox
      return true;
    } else if(obj instanceof Box) { //evaluate the RTT, if it is Box, then first type-cast and then 
      Box<?> objcpy = (Box<?>)obj; // typecast so that the 
      if(this.content == null) {//since this != obj, so they can't both be null.
        return false;
      } else {
        return this.content.equals(objcpy.content);//CTT, stores Object.equals(Object). At runtime, it finds that RTT of this.content is Box, and invokes this specific method
      }
    } else {
      return false; // obj is not instance of box.
    }
  }

  @Override
  public String toString() {
    if (this.content == null) {
      return "[]";
    }
    return "[" + content.toString() + "]";
  }

  public static <T> Box<T> of (T obj) { // for static methods
    if (obj == null) {
      return null;
    }
    return new Box<T> (obj);
  }

  public static <T> Box<T> empty() {
    @SuppressWarnings("unchecked")
    Box<T> emptyBox = (Box<T>) EMPTY_BOX;
    return emptyBox;
  }

  public boolean isPresent() {
    return this.content != null;
  }

  public static <T> Box<T> ofNullable(T obj) {
    if(obj == null) {
      return empty();
    }
    return of(obj);
  }

  public Box<T> filter(BooleanCondition<? super T> bc ) {
    if (this== EMPTY_BOX) {
      @SuppressWarnings("unchecked")
      Box<T> emptyBox = (Box<T>)EMPTY_BOX;
      return emptyBox;
    }
    if(bc.test(this.content)) {
      return this;
    } else {
      return empty();
    }
  }

  public <U> Box<U> map (Transformer<? super T, U> transformer) {
    if (this == EMPTY_BOX) {
      @SuppressWarnings("unchecked")
      Box<U> emptyBox = (Box<U>)EMPTY_BOX;
      return emptyBox;
    }
    U newContent = transformer.transform(this.content);
    if (newContent == null) {
      @SuppressWarnings("unchecked")
      Box<U> emptyBox = (Box<U>)EMPTY_BOX;
      return emptyBox;
    }
    return new Box<U>(newContent);
  }
}

Immutable

immutable means unable to modify any of the value inside the class from outside. It uses several principles to ensure this:

  1. private fields.
  2. final to signal that we don't want to attach another value onto it.
  3. never directly use an object passed from outside, first make a copy instead.
  4. getter function returns a copy.

With immutability ensured, we can infer several good features.

  1. No class fields can be ever modified from outside, even the content inside the instance a field is referring to. So we can safely share the instance itself and use getter() function.
  2. The program is easier to understand.
  3. It supports concurrent programing since the order of execution does not affect how the program is running.
  4. It enables memory sharing, i.e. reusing objects since they cannot be modified even if we point another pointer to it, to optimize the program.

nested classes

  1. inner class (non-static nested class): class directly inside another class
  2. static nested class: same to inner class, but static. So it can only visit static fields inside the outer class.
  3. local class: class directly inside a method. Variable capture is used here.

The most important concept of nested classes is variable capture. Since the function call is implemented on stack, when the call ends, the stack will be cleared. But the instance remains, so it needs to make a copy of the variables of the method. However, the method fields may change over execution, and it is hard to figure out which value to use. So Java decides that only final fields or fields can not ever be modified are copied. since method M is inside a class A, the local class visit fields and methods inside class A by capturing A.this from the method.

Variable capture also applies to lambda expressions if it is directly defined in a method.

In ordinary local classes, this refers to self, and A.this refers to its an instance of the outer class where the local class is instantiated. But for lambda expressions, this refers to its outer class, and one can only access itself by its variable name.

monad & functor

可以参考以下网页

https://zhuanlan.zhihu.com/p/359081403

monad相对于其他functor的特点是拥有更好地处理side information的能力。并不是说其他functor完全做不到,只是monad的封装可以让代码更加简洁robust。

functor

建立内部数据的属性(容错,lazy等),并且允许使用纯函数式编程进行数值的转换,同时其接口允许链式调用,编写程序更加容易。

有关上下文的functor除了monad还有applicative functor,值的存在与否(some, none) 也算是side information。

functor要符合以下两个性质:

  1. identitiy law: functor.map(x -> x) = functor 进行无变化操作后functor的结构和内容不变。

  2. composition law: functor.map(x -> f(x)).map(x -> g(x)) = functor.map(x -> g(f(x))) 保证了链式调用的正确性。

monad(将值嵌入到上下文中)

f(x)为一个从x的类型映射到Monad类型的函数,side information如log等是有累加关系的。monad是Monad类型的一个object。

一定要符合下面三个性质:

  1. left identity law: Monad.of(x).flatmap(x -> f(x)) == f(x) 把一个x放入上下文后再flatmap f和直接f一样。
  2. right identity law: monad.flatmap(x -> Maybe.of(x)) == monad 再次打包内容和side information不变。
  3. associative law: monad.flatMap(x -> f(x)).flatMap(x -> g(x)) == monad.flatMap(x -> f(x).flatMap(y -> g(y))) 方便进行链式计算,方便代码重构。

Monad 运算律主要关心副信息的一致性和传播规则,而内部的值在这些运算中只是被传递和转换,不直接干扰这些定律的满足情况。这也是为什么在设计 Monad 时,我们会特别强调 ofunit 操作要“中性”,以及 flatMap 操作需要严格地将上下文按照预期组合。

例子:

For the first seven questions, consider the class A below.

class A {
  private final int value;
  private final int cumulative;

  public A(int value, int cumulative) {
    this.value = value;
    this.cumulative = cumulative;
  }

  public static A of(int value) {
    return new A(value, 0);
  }

  public A flatMap(Transformer<Integer, A> transformer) {
    A updated = transformer.transform(this.value);
    return new A(updated.value, updated.cumulative + this.cumulative);
  }

  public String toString() {
    return this.value + " " + this.cumulative;
  }
}

If we change the factory method of A to

  public static A of(int value) {
    return new A(value, 1);
  }

which of the following laws would A violate?

(select all correct options)
A. The left identity law.
B. The right identity law.
C. The associative law.

不符合 left identity law 因为 A.of(x).flatmap(x -> f(x)),简化后为new A(updated.value, updated.cumulative + 1)。而f(x)所包含的是updated.cumulative,所以其side information是不相同的。

不符合right identity law 因为 a.flatmap(x -> Maybe.of(x)),其side information是a的side information加1,与a的side information不同。

符合 associative law 因为 a.flatMap(x -> f(x).flatMap(y -> g(y))) 中 .of 没有参与运算,所以 side information 与 a.flatMap(x -> f(x).flatMap(y -> g(y))) 相同。

命名问题

在函数式编程中,FunctorMonad 的实现和应用通常只是数学上更为丰富和严格的定义的一个“子集”或简化版本。主要体现在以下几个方面:

  1. 抽象层次的取舍:
    数学中,Functor 是范畴之间的映射,需要满足非常一般化的性质,而在编程中,我们通常只关注容器类型如何映射内部值,这里只实现了 map 方法及其最基本的律。类似地,Monad 在范畴论中涉及很多细致的概念(比如自然变换、上同态等),而编程中则更注重如何顺序地组合具有上下文依赖的计算。
  2. 只关注必要的性质:
    编程语言(如 Haskell 或 Scala)中的 Functor 和 Monad 类型类主要关心满足特定的运算律(例如 Functor 的映射律,Monad 的左单位、右单位和结合律),这些律足以保证代码组合和重构时的正确性和一致性,而不必实现或验证所有数学上可能讨论的属性。
  3. 实践中的折中:
    为了让抽象概念在实际开发中易于使用和理解,编程社区对这些概念做了不少简化和具体化。这样一来,它们就能满足大多数应用场景,而无需理解全部的范畴论背景。换句话说,我们只采纳了数学概念中那些对软件设计和代码组合最有用的部分。

总结来说,尽管在数学上 Functor 和 Monad 具有更广泛、复杂的理论背景,但在编程中的应用通常只取其中最核心、最实用的一部分,这既方便了开发者理解和使用,也使得它们能灵活地适应各种编程需求。

functor 和 Monad 的核心性质其实就是其定义本身。

miscellaneous

  1. Java parameter uses pass by value. If we pass a primitive, the value will be copied. If pass an object, it will create another reference, namely create a Object tmp = p. It can modify the instance fields that p is pointing to, but cannot change the fact that p is pointing to the instance.

  2. LSP: We should never violate the contract of the father class when writing the child class. These contracts can be written in documentation, comments, or as an implicity acknowledgement. For example, in Java documentation, equals method must satisfy reflexitivity, symmetry and transitivity.

  3. class A {
        public void methodA() {
            System.out.println("A method");
        }
    }
    
    class B extends A {
        public void methodB() {
            System.out.println("B method");
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            // 创建一个A类的实例
            A a = new A();
            // 下面的强制类型转换虽然在编译时通过,但在运行时会抛出 ClassCastException
            B b = (B)a;  // 运行时错误: ClassCastException
        }
    }
    
    
  4. If an abstract class A is implementing or inheriting class B, it need not implement the abstract methods. But concrete classes should implement all abstract methods.

  5. Java is using pass-by-value. Although it seems that you can modify the object if you pass in the namle pointing to the object, this is just a copy of the name. However, in pass-by-referrence, you will make a alias of the name, you can even unbind the object with the name using the alias.

  6. Interface中所有method隐含public,override时我们要使access modifier相同,method signature一样,返回类型和exception为父类中的返回类型子类型。

CS2030S EX5 problems and fixes

If the parent abstract class doesn't have throws, the child cannot add throws for checked exceptions, but can add for unchecked exceptions. Child class'sthrows should also be <: parent class's throws.

public abstract class Box<T> {
    public abstract T get();
}
public class ObjectBox extends Box<Object> {
    // override T get();
    @Override
    public Object get() {
		return ...;
    }
}

本文存在大量AI生成内容,但都经过我的仔细审查。如果仍有问题可以留言。

Comments

No comments yet.