项目开发笔记-3

面向对象

Posted by Sirin on October 31, 2025

关于方法(Method)与构造函数(Constructor)

特性 静态方法 实例方法
关键字 static static关键字
调用方式 类名.方法名() 对象名.方法名()
内存位置 方法区 堆内存
访问实例变量 不能直接访问 可以访问
  • 当类中没有定义任何构造函数时,Java会自动提供默认构造函数
  • 在构造函数中使用this()调用其他构造函数时,this()必须是构造函数的第一条语句
  • 构造函数中最好进行参数的验证并抛出适当的异常
  • 在构造函数中不要调用可重写的方法,因为子类可能重写这些方法,而此时对象没有完全初始化,可能会导致不可预期的行为
  • 当构造函数参数过多时,可以调用Builder Pattern来简化
  • 防御性复制是指在构造函数中创建可变对象参数的副本,而不是直接复制引用,避免因为外部代码对原对象进行修改而影响类的内部状态
  • 构造函数链式调用可以减少代码重复,方便维护,比如将通用的初始化逻辑集中在一个构造函数内,其他构造函数通过this()调用它。

权限控制

访问修饰符 同一个类 同一个包 不同包的子类 不同包的非子类
public
protected
private
默认
⚠️注意

在接口(interface)中,所有方法默认都是public的,因为接口的目的是定义公共契约,供实现类使用,所以接口中的字段都是默认public(完全公开,对于所有使用者可见) static(不能被实例化,属于接口本身) final(不可变,保证其稳定性)的。

关于this关键字

// 1. 参数与成员变量同名时使用this
public void setName(String name) {
    this.name = name;		// 消除命名歧义必须使用this
}

// 2. 构造方法重载时使用this()
public Person(String name) {
    this(name, 0);		// 构造方法重载,避免代码重复
}

// 3. 方法链式调用返回this
public Builder setValue(String value) {
    this.value = value;
    return this;		// 链式调用,提供流畅API
}

// 4. 提高代码可读性
public void process() {
    this.scan();		// 明确表示当前对象操作
    this.execute();
  	this.print();
}
⚠️注意
  • 静态方法和静态代码块中不可以使用this(因为静态方法不属于任何对象实例)
  • this()必须是构造方法的第一条语句
  • 避免构造方法之间出现循环调用
  • this是用于引用当前对象,super是引用父类

关于String

核心特性

  • 不可变性:String对象一经创建,内容不可修改,和golang一样
  • 字符串池:JVM会维护一个字符串常量池来优化内存使用
  • 线程安全:由于其不可变性,String天生是线程安全的

关于String的运算与函数操作

// 一些例子
String str1 = "Hello";
String str2 = "Hello";	// 字面量创建,效率最高
String str3 = new String("Java");		// 创建新对象,效率较低
String str4 = new String("Java");

System.out.println(str1 == str2);		// true 字面量创建的String指向同一个对象
System.out.println(str3 == str4); 	// false
System.out.println(str3.equals(str4));	//true
System.out.println(str3.equalsIgnoreCase(str4));	// 忽略大小写
int compareRes = str2.compareTo(str3);	// 比较长度

// 获取长度
int length = text.length();

// 安全的判空方法
if (text != null && !text.isEmpty()) {
  System.out.println("The string is not blank");
}

// 查找字符和子串
int idx1 = text.indexOf('e');		// 首个出现位置
int idx2 = text.indexOf("Apple");	// 首个子串出现位置的首位下标
int idx3 = text.lastIndexOf('e');	// 最后出现位置

// 包含判断
boolean contains = text.contains("World"); // true
boolean startsWith = text.startsWith("Hello"); // true
boolean endsWith = text.endsWith("World"); // true

当需要进行频繁的字符串修改时,应使用StringBuilder或者StringBuffer

特性 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 安全 不安全 安全
性能 频繁修改时差 较快
使用场景 不变 单线程频繁修改 多线程频繁修改

关于String.intern()方法

  • 作用:将字符串对象添加到字符串常量池中,并返回常量池中对应字符串的引用
  • 用法:str2 = str1.intern()
  • 注意要谨慎使用该方法,避免字符串常量池过大,出现内存泄漏

关于包(Package)

  • package语句必须是第一个非注释语句
  • 一个Java文件只能有一个package
  • 包名必须与目录结构完全对应
  • 如果没有package语句,类属于默认包
  • 包名应该全部用小写字母,避免使用Java关键字,不能以数字开头
  • import java.util.*表示导入包中所有公共类和接口,不包括子包和静态成员
  • java.lang中的包会被自动导入,无需显式import
  • 静态导入使用import static关键字,允许直接使用静态方法和字段而无需类名前缀

关于继承

protected关键字就是为了继承设计的,允许子类访问父类的成员,但是不允许其他包中的非子类访问。

下面是一个例子

public class Animal {
	protect String name;
  protect String species;
  
  public Animal(String name, String species) {
  		this.name = name;
    	this.species = species;
  }
  
  public void makeSound() {
    	System.out.println(name + "is making sounds.")
  }
  
}

在另一包内,我们可以继承父类,构建一个猫科的类

public class Cat extends Animal {
  // 采用extends关键字来表示继承
  // final修饰的类不能被继承(e.g., String类)
  
  private String breed;
  
  
  // 创建子类对象时,会先调用父类构造方法,再调用子类构造方法
  public Cat(String name, String breed) {
    	// 如果父类没有无参构造方法
    	// 子类必须在构造方法的第一行显式调用父类的有参构造方法
    	super(name, "felines");
    	this.breed = breed;
  }
  
  // 重写父类中的方法
  // 重写方法的访问修饰符可以更宽松,但不能更严格
  // 重写方法的方法名和参数列表完全相同,返回类型可以是父类方法返回类型的子类
  // Object类是所有类的根父类
  @Override
  public void makeSound() {
    	System.out.Println(name + " Meow.")
  }
  
}

关于抽象类(abstract)与接口(interface)

抽象类

抽象类不能被直接实例化,一般是用于定义一组相关类的通用特征和行为,同时强制子类实现特定的方法。一般是用于多个类需要共享代码时,本身提供部分实现,剩余留给子类。

abstract class Shape {
    protected String color;
    protected String name;
    
  	// 构造方法 
    public Shape(String name, String color) {
        this.name = name;
        this.color = color;
    }
    
    // 抽象方法:计算面积
  	// 在不同的图形子类中可以有不同的实现
    public abstract double calculateArea();
    
    // 抽象方法:计算周长
    public abstract double calculatePerimeter();
    
    // 具体方法:显示图形信息
    public void displayInfo() {
        System.out.println("Graphics: " + name);
        System.out.println("Color: " + color);
        System.out.println("Area: " + String.format("%.2f", calculateArea()));
        System.out.println("Perimeter: " + String.format("%.2f", calculatePerimeter()));
    }
}
⚠️注意
  • abstract不能和final同时使用,因为前者需要被继承,而后者不能被继承,两者是矛盾的。
  • 如果一个类继承了抽象类,但没有实现所有的抽象方法,那么这个类也必须声明为抽象类,否则会编译错误

接口

接口就是一个完全抽象的类,只包含常量(默认public static final)抽象方法(默认public abstract)的声明。从Java 8开始,接口还可以包含默认方法(default关键字)和静态方法(static关键字),接口定义了类应该做什么,但是不规定怎么做。

默认方法允许在接口中提供方法的默认实现,例如:

interface Collection {
    void add(Object item);
    void remove(Object item);
    int size();
    
    // 默认方法
    default boolean isEmpty() {
        return size() == 0;
    }
    
    default void clear() {
        // 默认的清空实现
        while (!isEmpty()) {
            // 需要子类提供具体的移除逻辑
        }
    }
}

静态方法则是属于接口本身,能够通过接口名来调用,不能被实现类重写。

接口支持实现多重继承,即一个子类可以实现多个接口的方法。同时,接口也支持继承其他接口,但需要使用的是extends关键字,例如interface B extends A {},不能使用implements关键字。

public interface Interface1 {
  // public static final
  int CONSTANT = 6;
  
  // public abstract
  int method1();
  void method2();
}

// 多重继承
public class MyClass implements Interface1, Interface2 {
  @Override
  public int method1() {
    	// 实现interface1中的方法
  }
  
  @Override
  public void method2() {
    // 实现interface1中的方法
  }
  
  @Override 
  public void method3() {
    // 实现interface2中的方法
  }
}

抽象类和接口的对比

特性 抽象类Abstract Class 接口Interface
关键字 abstract class interface
继承方式 extends,只能单继承 implements,可以多重继承
方法实现 抽象方法+具体方法 抽象方法+默认方法+静态方法
变量形式 可以有实例变量和常量 只能是public static final的常量
构造方法 可以有构造方法 不能有构造方法
访问修饰符 可以有各类访问修饰符 默认public

关于多态

多态允许不同类的对象对同一消息作出不同的响应,主要通过方法重写和动态绑定来实现。多态的实现条件包括:

  • 继承关系:子类继承父类
  • 方法重写:子类重写父类的方法
  • 向上转型:父类引用指向子类对象

多态分为两种,一种是静态多态(编译时多态),一种是动态多态(运行时多态)

// 静态多态: 方法重载Overloading, 运算符重载,编译时确定
public void print(int num) {}
public void print(String str) {}
public void print(boolean flag) {}


// 动态多态:方法重写Overriding, 动态绑定,运行时确认调用哪个方法
@Override 
public void makeSound() {
  System.out.println("Meow");
}

向上转型Upcasting

// 向上转型: 将子类对象复制给父类引用
Animal animal = new Dog("旺财", 3, "金毛");  // 自动向上转型

// 特点:
// 1. 自动进行,无需强制转换
// 2. 只能访问父类中定义的方法
// 3. 实际调用的是子类重写的方法(动态绑定)
animal.makeSound();  // 调用Dog重写的makeSound方法
animal.move();       // 调用Dog重写的move方法
// animal.wagTail(); // 编译错误!无法访问子类特有方法

向下转型Downcasting

// 向下转型: 将父类引用转化为子类引用
Animal animal = new Dog("旺财", 3, "金毛");

// 安全的向下转型
if (animal instanceof Dog) {
    Dog dog = (Dog) animal;  // 强制转换
    dog.wagTail();           // 现在可以调用子类特有方法
    dog.guard();
}

// 错误示例(会抛出ClassCastException)
// Cat cat = (Cat) animal;  // 运行时异常!

instanceof关键字用于检查对象是否是特定类的实例,其用法是object instanceof ClassName,在用与判断能否进行向下转型时十分有用。同时,它也能用于判定对象是否实现了特定的接口,object instanceof InterfaceName

⚠️注意
  • instanceOf对于null值总是会返回false,提供了null安全性
  • Java中的数组也是Object的子类型
  • 需要调用对象的通用方法时,应该使用多态,而不是instanceof

关于内部类

成员内部类 Member Inner Class

定义在外部类中的非静态类,自动持有一个指向其外部类实例的隐式引用,可以访问外部类中的所有成员不能定义静态成员

由于内部成员类持有外部类的隐式引用,如果其对象的生命周期比外部类更长,容易导致外部类无法被垃圾回收,造成内存泄露

public class Outer {
    private int value = 10;
    
    public class Inner {
	      private int innerVal = 42;
        public void print() {
            System.out.println(value); // 直接访问外部类字段
          	// 可以通过Outer.this.value访问
        }
    }
}

静态嵌套类 Static Nested Class

静态嵌套类使用static关键词修饰,可以访问外部类中的静态变量,但不能访问其成员变量可以独立于外部类对象存在

比如Builder类就是采用了静态嵌套类,因为它不需要访问外部类的实例成员。

public class OuterClass {
    private static String staticField = "外部类静态字段";
    private String instanceField = "外部类实例字段";
    
    // 静态嵌套类
    public static class NestedClass {
        private String nestedField = "嵌套类字段";
        
        public void nestedMethod() {
            // 可以访问外部类的静态成员
            System.out.println(staticField);
            
            // 不能直接访问外部类的实例成员
            // System.out.println(instanceField); // 编译错误
            
            System.out.println(nestedField);
        }
    }
}

// 独立于外部类对象,直接创建静态嵌套类对象
OuterClass.NestedClass nested = new OuterClass.NestedClass();
nested.nestedMethod();

局部内部类 Local Inner Class

局部内部类是定义在方法或者代码块里面的类,可以访问外部类字段和final局部变量。

public class ScopeComparison {
    private String outerField = "outer";
    
    // 成员内部类 - 类级别作用域
    class MemberInner {
        // 在整个外部类中可见
    }
    
    public void demonstrateScopes() {
        String localVar = "local";
        
        // 局部内部类 - 方法级别作用域
        class LocalInner {
            public void show() {
                System.out.println(outerField); // 可以访问外部类字段
                System.out.println(localVar);   // 可以访问final局部变量
            }
        }
        
        LocalInner inner = new LocalInner();
        inner.show();
        
        // 局部内部类只在当前方法中可见
        // 外部无法访问:LocalInner inner2 = new LocalInner(); // 编译错误
    }
    
    public void anotherMethod() {
        // 这里无法访问LocalInner类
        // LocalInner inner = new LocalInner(); // 编译错误
    }
}

匿名类 Anonymous Class

匿名类没有显式的类名,直接通过new关键词来进行创建,必须继承自一个父类或者实现一个接口,不能有显式的构造函数。匿名类可以访问外部类的实例成员和final的局部变量。

// 定义接口
interface Greeting {
    void sayHello(String name);
    void sayGoodbye(String name);
}

public class AnonymousDemo {
    public static void main(String[] args) {
        // 创建匿名类实现接口
        Greeting greeting = new Greeting() {
            private int count = 0;
            
            @Override
            public void sayHello(String name) {
                count++;
                System.out.println("你好, " + name + "! (第" + count + "次问候)");
            }
            
            @Override
            public void sayGoodbye(String name) {
                System.out.println("再见, " + name + "!");
            }
            
            // 可以添加额外的方法(但外部无法调用)
            public void internalMethod() {
                System.out.println("内部方法");
            }
        };
        
        greeting.sayHello("张三");
        greeting.sayHello("李四");
        // greeting.internalMethod(); // 编译错误 - 只能访问接口定义的方法
    }
}

关于枚举

枚举是一种特殊的类,其自动继承java.lang.Enum类,用于定义一组固定的常量,提供了类型安全的常量定义方式。

枚举构造方法必须是private的(可以省略private关键字),这保证了枚举常量只能在枚举内部创建,维护了其单例特性。

// 最基本的枚举 Day.SUNDAY
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY, 
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

// 带有属性的枚举
public enum Color {
    RED("红色", "#FF0000"),
    GREEN("绿色", "#00FF00"),
    BLUE("蓝色", "#0000FF");
    
    private final String name;
    private final String code;
    
    Color(String name, String code) {
        this.name = name;
        this.code = code;
    }
}

// 带方法的枚举
public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        public double apply(double x, double y) {
            return x - y;
        }
    };
    
    public abstract double apply(double x, double y);
}

枚举提供了一系列的内置方法

方法 说明 示例 返回值
name() 枚举常量名称 Color.RED.name() “RED”
ordinal() 枚举常量的声明顺序序号 Color.RED.ordinal() 0
toString() 枚举常量字符串表示 Color.RED.toString() “RED”
valueOf(String) 根据名称获取枚举常量 Color.valueOf("RED") Color.RED
values() 返回所有枚举常量数组 Color.values [RED, GREEN, BLUE]
CompareTo() 比较枚举常量顺序 Color.RED.compareTo(Color.BLUE) 负数

关于反射 Reflection

反射机制允许程序在运行时检查和操作类、接口、字段和方法的信息。通过反射,我们可以在运行时动态地创建对象、调用方法、访问和修改字段,甚至可以获取类的结构信息。

使用反射的第一步是获取Class对象

  • Class clazz = 类名.class:编译时确定,效率最高
  • Class clazz = 对象.getClass():运行时获取
  • Class clazz = Class.forName(String):动态加载类,适合类名以字符串提供的情况。

获取所有公共字段(包括继承的): Field[] publicFields = clazz.getFields()

获取所有声明的字段(不包括继承的): Field[] declaredFileds = clazz.getDeclaredFields()

设置可访问私有字段:privateField.setAccessible(true)

修改私有字段的值:privateField.set(obj, newVal)

关于方法和构造器的反射操作这里就不再赘述了,一个是Method类型,一个是Constructor类型。

需要注意的是,反射操作性能低,最好要进行异常处理,并且缓存获取的类、方法、字段对象来提高性能。

关于Java注解Annotations

@Override标记重写父类的方法

@Deprecated标记已废弃的类、方法或字段

@SuppressWarnings抑制编译器警告信息

@SafeVarargs标记安全的可变参数方法,抑制堆污染警告

@FunctionInterface标记函数式接口,确保接口只有一个抽象方法(不会有多个抽象方法,也不能没有抽象方法,否则会编译错误)

关于泛型Generics

泛型允许用户在编写代码时可以使用“类型参数”,在实际使用时再指定具体的类型。比如

// 没有泛型的情况
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); // 自动类型转换,不需要强制转换

类型安全(编译时检查)

// ❌ 没有泛型 - 运行时可能出错
List list = new ArrayList();
list.add("字符串");
list.add(123); // 编译通过,但运行时可能出问题

// ✅ 使用泛型 - 编译时就能发现错误
List<String> stringList = new ArrayList<>();
stringList.add("字符串");
// stringList.add(123); // 编译错误!无法添加整数

消除强制类型转换

// 传统方式
List list = new ArrayList();
list.add("hello");
Object obj = list.get(0);
String str = (String) obj; // 需要强制转换

// 泛型方式
List<String> list = new ArrayList<>();
list.add("hello");
String str = list.get(0); // 自动转换,无需强制转换

泛型的使用方式:

  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;
        }
           
        public String toString() {
            return "Box containing: " + content;
        }
    }
       
    // 使用泛型类
    public class GenericClassDemo {
        public static void main(String[] args) {
            // 创建不同类型的Box
            Box<String> stringBox = new Box<>("Hello");
            Box<Integer> integerBox = new Box<>(42);
            Box<Double> doubleBox = new Box<>(3.14);
               
            System.out.println(stringBox.getContent());   // Hello
            System.out.println(integerBox.getContent());  // 42
            System.out.println(doubleBox.getContent());   // 3.14
               
            // 编译时类型检查
            // stringBox.setContent(123); // 编译错误!
        }
    }
    
  2. 泛型接口
    // 定义泛型接口
    public interface Repository<T> {
        void save(T entity);
        T findById(Long id);
        List<T> findAll();
        void delete(T entity);
    }
       
    // 实现泛型接口
    public class UserRepository implements Repository<User> {
        private List<User> users = new ArrayList<>();
           
        @Override
        public void save(User user) {
            users.add(user);
        }
           
        @Override
        public User findById(Long id) {
            return users.stream()
                       .filter(user -> user.getId().equals(id))
                       .findFirst()
                       .orElse(null);
        }
           
        @Override
        public List<User> findAll() {
            return new ArrayList<>(users);
        }
           
        @Override
        public void delete(User user) {
            users.remove(user);
        }
    }
       
    // 使用泛型接口
    public class RepositoryDemo {
        public static void main(String[] args) {
            Repository<User> userRepo = new UserRepository();
            User user = new User(1L, "张三");
            userRepo.save(user);
               
            User found = userRepo.findById(1L);
            System.out.println(found.getName()); // 张三
        }
    }
    
  3. 泛型方法
    public class GenericMethodDemo {
           
        // 泛型方法 - 在返回类型前声明类型参数
        public static <T> T getFirstElement(List<T> list) {
            if (list == null || list.isEmpty()) {
                return null;
            }
            return list.get(0);
        }
           
        // 多个类型参数的泛型方法
        public static <K, V> void printPair(K key, V value) {
            System.out.println("Key: " + key + ", Value: " + value);
        }
           
        // 有界泛型方法 - 限制类型参数的范围
        public static <T extends Number> double sum(List<T> numbers) {
            double total = 0.0;
            for (T number : numbers) {
                total += number.doubleValue();
            }
            return total;
        }
           
        // 使用通配符的泛型方法
        public static void processList(List<?> list) {
            for (Object obj : list) {
                System.out.println(obj);
            }
        }
           
        public static void main(String[] args) {
            // 使用泛型方法
            List<String> strings = Arrays.asList("A", "B", "C");
            String firstString = getFirstElement(strings);
            System.out.println(firstString); // A
               
            List<Integer> integers = Arrays.asList(1, 2, 3);
            Integer firstInt = getFirstElement(integers);
            System.out.println(firstInt); // 1
               
            // 多个类型参数
            printPair("姓名", "李四");
            printPair(123, 456.78);
               
            // 有界泛型
            List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
            double total = sum(ints);
            System.out.println("总和: " + total); // 15.0
               
            // 通配符
            processList(strings);
            processList(ints);
        }
    }
    

泛型中拥有?通配符,表示未知类型,主要有三种形式

  • 上界通配符:? extends T,只能读取,不能写入(生产者)
  • 下界通配符:? super T,只能写入,不能读取(消费者)
  • 无界通配符:?,只能进行与类型无关的操作(例如判断是否为空,大小)
// 上界通配符 - 只能读取
public static double sum(List<? extends Number> numbers) {
    double total = 0.0;
    for (Number num : numbers) {
        total += num.doubleValue(); // 只能读取
    }
    return total;
}

// 下界通配符 - 只能写入
public static void addNumbers(List<? super Integer> numbers) {
    numbers.add(1); // 只能写入Integer或其子类
    numbers.add(2);
    numbers.add(3);
}

// 无界通配符 - 类型无关操作
public static int getSize(List<?> list) {
    return list.size(); // 只能进行与类型无关的操作
}
⚠️注意

泛型是不变的,即使StringObject的子类,List<String>List<Object>之间也没有继承关系,这是为了保证类型安全。

由于泛型是采用类型擦除来实现的,这意味着泛型信息仅存在于编译时,运行时会被删除,所以

  • 运行时会丢失类型信息,所以运行时不能获取泛型的具体类型信息,通过.getClass()获取其Class对象都是ArrayList.class
  • 不能创建泛型数组 new T[10]
  • 不能实例化类型参数new T()
  • 不能使用基本类型作为类型参数,也不能在静态上下文里面使用类型参数