初始化和清理
构造器
构造器是在创建对象时,将类的成员属性进行初始化过程的一种函数
定义
1 2 3 4 5
| class Rock { Rock(){ System.out.print("Rock"); } }
|
当需要进行实例化创建时,类会根据传入的参数列表选择构造器进行对象的初始化。
这个叫做 构造器重载。 通过不同的函数列表来决定 调用哪个构造器。
当未定义构造器时,实际上是执行了一个无参的构造器。
构造方法并不负责“创建对象”本身,它负责的是“初始化已经分配好的对象”
也就是说:”new” 才是真正负责对象创建和内存分配的关键,构造方法是“后处理器”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class Rock{ public static void main(String[] args) { new Rock(); new Rock(1); new Rock(1.0); }
public Rock() { System.out.println("Rock without parameter"); } public Rock(int i) { System.out.println("Rock with int parameter"); } public Rock(double d) { System.out.println("Rock with double parameter"); } }
|
this关键字
在构造方法或者实例方法中,如果需要调用同一个类定义下的结构(属性/方法)
可以使用this关键字进行调用,this作为创建后的实例对象的引用
构造器中调用构造器
必须在构造器的第一行中调用另一个构造器
其实很类似于python中的默认参数的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class Flower { int petalCount = 0; String s = "initial value"; Flower(int petals) { petalCount = petals; System.out.println("Constructor w/ int arg only, petalCount = " + petalCount); } Flower(String ss){ System.out.println("Constructor w/ String arg only, s = " + ss); } Flower(String s, int petals) { this(petals); this.s = s; System.out.println("String & int args"); } Flower() { this("hi", 47); System.out.println("default constructor"); } void printPetalCount() { System.out.println("petalCount = " + petalCount + " s = " + s); } public static void main(String[] args) { Flower x = new Flower(); x.printPetalCount(); } }
|
初始化
在Java中
类成员变量,静态成员变量。都会在其具体的被执行过程中被JVM默认初始化
值类型 被初始化为0 , 引用类型被初始化为null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Flower { int petalCount ; static Piece p ;
public static void main(String[] args) { Flower flower = new Flower(); System.out.println(flower.petalCount); System.out.println(p); } }
class Piece{ int i = 90 ;
}
|
而对于局部变量 (即参与函数执行的变量)必须在java文件中就进行相应的变量定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Flower { public static void main(String[] args) {
} public static void sayHello(){ int i ; System.out.println(i); }
}
|
因为在编译时
对于类的行为 :编译器确认类的结构,对类中的成员变量进行初始化
对于方法的行为 : 先构建局部变量表,再对方法中定义的代码进行执行,强制局部变量必须手动初始化
初始化与初始化顺序
对于 变量 通过赋值操作符来进行变量的初始化
1 2 3 4 5 6 7 8 9
| public class InitMethod{ static int value1 = 1; int value2 = 2; public static void main(String[] arg){ } } class ObjectInstance{ char c = 'c'; }
|
或者在外部进行声明,在构造方法或者初始化代码块中进行初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class InitMethod{ static int value1 ; int value2 ; static { value1 = 1; }
{ value2 = 2; }; public static void main(String[] arg){ System.out.println(value1); System.out.println(new InitMethod().value2); }; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class InitMethod{ static int value1; int value2; InitMethod(){ value1 = 1; value2 = 2; } public static void main(String[] arg){ } } class ObjectInstance{ char c = 'c'; }
|
java中的 引用声明 虽然是自由的,但是必须要保证 声明发生在调用前
数组初始化
java中接受两种数组的声明
数组声明是一种引用类型,所以初始化会被初始化为null。并且a2=a1 意味着a2和a1指向同一个对象
数组的创建
1 2
| int[] a1 = {1,2,3} int[] a2 = new Integer[]{1,2,3}
|
可变参数列表
通过 type … object_name 来接收多个相同类型的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class OptionalTrailingArguments{ static void f(int required,String... trailing){ System.out.println("required:"+required+" "); for(String s:trailing){ System.out.println(s+" "); } } public static void main(String[] args) { f(1,"one"); f(2,"two","three"); f(0); }
}
|
枚举类型
定义了一个枚举。
枚举中的内容可以用于switch分支判断
.values() 遍历枚举类中的值
.ordinal() 返回值的顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class Select { public static void main(String[] args) { for(Option asd : Option.values()){ switch (asd){ case A: System.out.println("A"); break; case B: System.out.println("B"); break; case C: System.out.println("C"); break; case D: System.out.println("D"); break; case E: System.out.println("E"); default: System.out.println(asd.ordinal()); break; } } } }
enum Option{ A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z
}
|
类型推断
在方法中可以使用”var”来进行自动的类型推断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Plumbus{}
public class TypeInference { void method(){ String hello1 = "Hello"; var hello = "Hello"; Plumbus plumbus = new Plumbus(); var plumbus1 = new Plumbus();
} static void staticMethod(){ var hello = "Hello"; var plumbus1 = new Plumbus(); }
}
|
类中的字段类型必须在类加载时静态确定,以保证类的内存布局可被 JVM 构建。而 var
依赖编译器对赋值表达式进行类型推导,适用于方法体内的局部变量,不适合用于类字段这种结构性定义。
包
java中的引用管理路径是通过包声明来实现。
通过 反转的URL 来定义某个文件的路径
1 2 3 4 5 6 7 8 9
| package com.codfish.java.Localclass
📁 [classpath]/ └── 📁 com/ └── 📁 codfish/ └── 📁 java/ └── Localclass.java
|
实现隐藏
访问控制
public 声明本类/方法 是一个可以被任意调用的组件
private 声明 方法 在本类之外是不可见的,不可以被调用
protect 允许子类调用继承的父类中声明的protect方法
default(无声明) 类/方法 本文件中可以任意调用
类只有两种访问权限(public和 default )
通过访问控制,实现了一种功能和实现的解耦,调用者只需要关注声明为public的方法,因为他们只需要关注对象所提供的功能,而实现者更加关注于private方法 来更好的优化功能的实现。
对象设计
在面向对象编程中通常有这样几种 对象的关联关系
组合(has a)
多个对象作为另一个对象共同完成功能。通过组织这些对象的功能和执行流程来完成自身的功能
继承 (is a)
一个对象是另一个对象的实现,具备原对象的全部功能和属性。只是会根据不同的因素具体实现不同
委托 (delegates to)
一个对象作为中间代理人,将某个功能指定给另一个对象去完成。
final 关键字
final关键字用于将 引用 和 目标进行绑定
final 修饰类 该类不能被继承
final 修饰方法 则 该方法不能被重写
final 修饰变量 只能在声明处或者构造器中初始化 基础变量的值不能被更改,引用类型的 final 变量不能再指向其他对象,但原对象的内部状态仍可变化。
final 让“名字与所指对象”之间的绑定在编译期或类加载期就确定下来,不能再动态更换,失去了 Java 中多态或延迟绑定的一部分可能性。
对象创建过程中的加载
当创建对象时
如果类未被加载到jvm中,则需要从类初始化开始
查询父类是否进行加载
执行父类静态代码块,静态变量(按序加载)
执行子类静态代码块,静态变量(按序加载)
执行父类代码块,构造方法(按序加载)
执行子类代码块,构造方法(按序加载)
返回指向新创建的引用。结束创建过程
多态
多态提供了另一个维度的接口与实现分离
向上转型
在java中 允许将一个对象声明为其父类。而当调用时调用的则是子类中实现的对应的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class Polymorphism { public static void main(String[] args) { Animal animal = new Animal(); animal.makeSound();
Animal dog = new Dog(); dog.makeSound();
Animal cat = new Cat(); cat.makeSound(); } }
class Animal { public void makeSound() { System.out.println("The animal makes a sound"); } }
class Dog extends Animal { @Override public void makeSound() { System.out.println("The dog barks"); } }
class Cat extends Animal { @Override public void makeSound() { System.out.println("The cat meows"); } }
|
构造过程中的函数调用
在父类构造器中调用被子类重写的函数,依然调用的是子类重写的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } class Glyph { void draw() { System.out.println("Glyph.draw()"); }
public Glyph() { System.out.println("Glyph() before draw()"); draw(); System.out.println("Glyph() after draw()");
} }
class RoundGlyph extends Glyph { private int radius = 1;
RoundGlyph(int r) { radius = r; System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius); }
void draw() { System.out.println("RoundGlyph.draw(), radius = " + radius); } }
|
接口
抽象方法与抽象类
一些类的设计是为了定义共同的标准和形式,其本身以及定义的方法并不是用来进行实例化或者方法调用的。这样的类被称为抽象类。
java中也提供了抽象方法的声明
存在抽象方法的类被称为抽象类
当有类想继承抽象类时,作为一个实现类必须重写抽象类中的抽象方法,作为抽象类则可以直接继承
接口
接口是面向设计的一种完全抽象,通过定义一系列的函数签名,使得符合该规则的类能够完成接口所描述的功能。
1 2 3 4
| interface AnInterface { void firstMethod(); void secondMetho(); }
|
在后续的java版本中 又允许向接口中添加 默认方法。这样可以在不改变接口设计的同时,向接口中扩充方法
1 2 3 4 5 6 7
| interfaceIneterfaceWithDefault{ void firstMethod(); void secondMethod(); default void newMethod(){ System.out.println("new Method"); } }
|
也可以向接口添加静态方法,意味着接口内可以包含供它使用的工具集
1 2 3 4 5 6 7 8 9 10
| public interface Operation { void execute(); static void runOps(Operation ... ops){ for(Operatoin op : ops) op.execute(); } static void show(String msg){ System.out.println(msg) } }
|
抽象类与接口
特性 |
接口 |
抽象类 |
组合 |
可以在新类中组合多个接口 |
只能继承一个抽象类 |
状态 |
不能包含字段 |
可以包含字段 |
默认方法与抽象方法 |
默认方法不需要在子类中实现 |
抽象方法必须在子类里实现 |
构造器 |
不能有构造器 |
可以有构造器 |
访问权限 |
隐式的public |
可以为protoct |
回到面向对象
在应用开发中,从表面上看,继承/多态、抽象类/方法以及接口似乎都能实现类似的功能。这不禁让我们思考:为何 Java 语言还要提供这些彼此相近的机制?
此时,我们不妨回到“面向对象”的核心理念来思考这个问题。在前文的讨论中我们提到,对象设计的关键在于:对象的行为是服务于特定功能的。换言之,对象所拥有的方法和属性,其本质目的是为了在业务系统中承载功能逻辑与状态维护。
在实际的业务场景中,一个对象往往具备多重功能。在某些特定流程中,对象自身的整体结构并不是最关键的,而是其中某个特定的行为或能力构成了流程的核心。当我们只关心这个特定行为时,我们实际上是在“抽象地使用这个对象的某个切面”。此时,对象的这个方法就成为了我们所关注的抽象能力——只要实现了这个方法,流程就能正常运行。
从这个角度出发,我们可以重新理解三种语言机制的区别:
- 继承与多态:通过继承机制,子类可以基于父类实现不同的行为,从而在不修改接口的前提下,通过多态支持多种功能实现。这强调的是“共性基础上的差异表达”。
- 抽象类与抽象方法:定义了某类对象在行为和状态上应具备的“模板”,为子类提供统一的结构约束与默认实现,强调的是“部分实现 + 行为约束”的组合设计。
- 接口(Interface):接口更专注于定义一组与业务流程相关的能力,它关注的是“对象能做什么”,而非“对象是什么”。接口强调的是功能契约,便于实现解耦与组合。
内部类
成员内部类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class Parcell{ class Content{ private int i = 11; public int value(){ return i; } } class Destination{ private String label; Destination(String whereTo){ label = whereTo; } String readLabel(){ return label; } }
public void ship(String dest){ Content c = new Content(); Destination d = new Destination(dest); System.out.println(d.readLabel()); } public static void main(String[] args) { Parcell p = new Parcell(); p.ship("Tasmania"); } }
|
将成员内部类的实例以函数调用返回。可以看到 内部类可以对类中的私有属性进行调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public class Sequence{ private Object[] items; private int next = 0; public Sequence(int size){ items = new Object[size]; } public void add(Object x){ if(next < items.length){ items[next++] = x; } } private class SequenceSelector implements Selector{ private int i = 0; @Override public boolean end(){ return i == items.length; } @Override public Object current(){ return items[i]; } @Override public void next(){ if(i < items.length){ i++; } } } public Selector selector(){ return new SequenceSelector(); } public static void main(String[] args) { Sequence sequence = new Sequence(10); for(int i = 0; i < 10; i++){ sequence.add(Integer.toString(i)); } Selector selector = sequence.selector(); while(!selector.end()){ System.out.print(selector.current() + " "); selector.next(); } } }
interface Selector{ boolean end(); Object current(); void next(); }
|
内部类可以通过 outter.this 获取到外部类的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class DotThis{ void f(){ System.out.println("DotThis.f()")} public class Inner{ public DotThis outer(){ return DotThis.this ; } } public Inner inner() {return new Inner();} public static void main(String[] args){ DotThis dt = new DotThis(); DotThis.Inner dti = dt.inner(); dti.outer().f(); } }
|
而 外部类可以通过 outter的实例 new 创建内部类的实例
1 2 3 4 5 6 7
| public class DotNew{ public class Inner{} public static void main(String[] args){ DotNew dn = new DotNew(); DotNew.Inner dni = dn.new.Inner(); } }
|
即通过外部类的.this 获取外部类的实例
通过外部类实例的.new 创建内部类的对象
局部内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Parcel5 { public Destination destination(String s ){ final class PDestination implements Destination{ private String label; private PDestination(String whereTo){ label = whereTo; } @Override public String readLabel(){return label;} } return new PDestination(s); } public static void main(String[] args){ Parcel5 p = new Parcel5(); Destination d = p.destination("Tasmania"); } }
|
匿名内部类
匿名内部类提供了 一种实现 即不用通过正式的类声明,而是直接通过实现接口方法的方式创建一个满足要求的对象来进行使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Parcel7{ public Contents contents(){ return new Contents(){ private int i = 11; @Override public int value(){return i; } }; } public static void main(String[] args){ Parcel7 p = new Parcel7(); Contents c = p.contents(); } }
|
设计内部类的目的 :
1. 逻辑关联和作用域管理
- 当一个类的定义仅对另一个类有意义时,就可以将它定义为内部类。
- 内部类的存在强化了“组合而非继承”的关系,让你表达“这个类只属于那个类”的语义。
2. 访问外部类的私有成员
- 内部类可以无障碍访问外部类的所有成员(包括
private
字段和方法)。
- 这提供了一种灵活的方式来“增强”外部类功能,而不依赖继承。
3. 封装和隐藏
- 内部类本质上是外部类的一部分实现细节,可以不暴露给外部用户。
4. 对事件处理/回调逻辑的支持
- 在 GUI 编程中(如 Swing),匿名内部类、局部内部类常用于事件监听器,实现响应逻辑。