关于方法(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); // 自动转换,无需强制转换
泛型的使用方式:
- 泛型类
// 定义泛型类 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); // 编译错误! } } - 泛型接口
// 定义泛型接口 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()); // 张三 } } - 泛型方法
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(); // 只能进行与类型无关的操作
}
泛型是不变的,即使String是Object的子类,List<String>和List<Object>之间也没有继承关系,这是为了保证类型安全。
由于泛型是采用类型擦除来实现的,这意味着泛型信息仅存在于编译时,运行时会被删除,所以
- 运行时会丢失类型信息,所以运行时不能获取泛型的具体类型信息,通过
.getClass()获取其Class对象都是ArrayList.class - 不能创建泛型数组
new T[10] - 不能实例化类型参数
new T() - 不能使用基本类型作为类型参数,也不能在静态上下文里面使用类型参数