【Java】面向对象课程笔记
本文为面向对象课程笔记
javac 和 java
JDK 和 JRE:
- JDK:Java SE Development Kit,标准开发包。
- JRE:Java Runtime Environment,运行环境。
- 使用 java 命令时,只需要有 JRE;使用 javac 命令时,需要有 JDK。
javac 和 java:
- javac 用于将 .java 文件编译成 .class 字节码文件。
-cp
或-classpath
或--class-path
选项:- 指定在何处查找用户类文件和注释处理器,该类路径覆盖 CLASSPATH 环境变量中的用户类路径。
- 如果未指定,则为当前目录。
-d
选项:- 设置类文件的目标目录。如果一个类是包的一部分,那么 javac 将该类文件放在一个子目录中,该子目录反映包名,并根据需要创建目录。
- 如果没有指定
-d
选项,那么 javac 会将每个类文件放在与生成它的源文件相同的目录中。 - 由
-d
选项指定的目录不会自动添加到用户类路径 CLASSPATH 中。
- java 用于执行。
-cp
或-classpath
或--class-path
选项。-ea
选项:启用 assertions。
参考:
- 菜鸟教程:Java 包(package)
- 博客园:Java知识点:javac命令
- 脚本之家:windows命令行中java和javac、javap使用详解(java编译命令)
- 文档:javac
- 文档:java
浅拷贝和深拷贝
浅拷贝和深拷贝:
- 浅拷贝:拷贝了源对象的地址,所以源对象的值发生变化时,拷贝对象的值也会发生变化。
- 深拷贝:拷贝了源对象的所有值,所以即使源对象的值发生变化时,拷贝对象的值也不会改变。
例:
- 对于基本数据类型,使用等号赋值时,改变原对象的值,不影响新对象的值。
- 对于引用数据类型:
- 使用等号赋值时,改变原对象的值,新对象的值全都被改变。
- 重写 clone 方法,直接调用
super.clone()
时(对应输出 1):- 类 A 中的 int 和 String 属性,在原对象值改变时,新对象值不会改变。
- 类 A 中的 Obj 属性,在原对象值改变时,新对象值也改变。
public class Test {
public static void main (String [] args) {
int a = 1;
int b = a;
a = 2;
System.out.printf("a = %d, b = %d\n\n", a, b);
String s1 = "hello";
String s2 = s1;
s1 = "HELLO";
System.out.printf("s1 = %s, s2 = %s\n\n", s1, s2);
A a1 = new A(1, "hello", new Obj(1, "hello"));
A a2 = a1;
a1.setA(2);
a1.setS("HELLO");
a1.getObj().setA(2);
a1.getObj().setS("HELLO");
System.out.printf("a1: %d %s\t\ta1.obj: %d %s\na2: %d %s\t\ta2.obj: %d %s\n\n",
a1.getA(), a1.getS(), a1.getObj().getA(), a1.getObj().getS(),
a2.getA(), a2.getS(), a2.getObj().getA(), a2.getObj().getS());
A a3 = new A(1, "hello", new Obj(1, "hello"));
A a4 = a3.clone();
a3.setA(2);
a3.setS("HELLO");
a3.getObj().setA(2);
a3.getObj().setS("HELLO");
System.out.printf("a3: %d %s\t\ta1.obj: %d %s\na4: %d %s\t\ta2.obj: %d %s\n\n",
a3.getA(), a3.getS(), a3.getObj().getA(), a3.getObj().getS(),
a4.getA(), a4.getS(), a4.getObj().getA(), a4.getObj().getS());
}
}
class A implements Cloneable{
int a;
String s;
Obj obj;
public A clone() { // 浅拷贝,直接调用父类的 clone()方法,对应输出 1
try {
return (A)super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
/* public A clone() { // 深拷贝,对应输出 2
try {
A clA = (A)super.clone();
clA.setObj(this.obj.clone());
return clA;
} catch (CloneNotSupportedException e) {
return null;
}
} */
// 省略构造方法、getter 和 setter
}
class Obj implements Cloneable{
int a;
String s;
public Obj clone(){
try {
return (Obj)super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
// 省略构造方法、getter 和 setter
}
输出 1:
a = 2, b = 1
s1 = HELLO, s2 = hello
a1: 2 HELLO a1.obj: 2 HELLO
a2: 2 HELLO a2.obj: 2 HELLO
a3: 2 HELLO a1.obj: 2 HELLO
a4: 1 hello a2.obj: 2 HELLO // 对应代码 40 行的浅拷贝
输出 2:
a = 2, b = 1
s1 = HELLO, s2 = hello
a1: 2 HELLO a1.obj: 2 HELLO
a2: 2 HELLO a2.obj: 2 HELLO
a3: 2 HELLO a1.obj: 2 HELLO
a4: 1 hello a2.obj: 1 hello // 对应代码 48 行的深拷贝
深拷贝的实现:
-
重写 clone 方法。
public A clone() { try { A clA = (A)super.clone(); clA.setObj(this.obj.clone()); return clA; } catch (CloneNotSupportedException e) { return null; } }
-
又是接口又是序列化的,看不懂,咕咕咕~
参考:
类的初始化顺序
例:
public class Test {
static {
System.out.println("一、在第一次构造类的实例时加载类(类A)");
}
static A a1;
static A a2 = new A(2);
public static void main(String [] args) {
System.out.println("\n-----MAIN START-----\n");
System.out.println("二、或在第一次访问类的静态方法时构造类(类C)");
C c1;
C.func();
System.out.println("三、或在第一次访问类的静态变量时构造类(类D)");
B b9 = D.b8;
System.out.println("四、再次构造类的实例时不会加载类的静态变量和静态代码块");
a1 = new A(1);
new C(1);
new D(1);
System.out.println("五、非静态变量和非静态代码块在访问类的静态方法或静态变量时不会加载");
B b10 = C.b7;//这一行没有输出
D.func();
System.out.println("\n-----MAIN END-----");
}
}
class A {
{
System.out.println("\tA2、非静态变量和非静态代码块\n\t\t非静态代码块1");
}
static {
System.out.println("\tA1、静态变量和静态代码块");
}
static B b1 = new B(1);
static B b2;
static B b3;
static {
b2 = new B(2);
}
static B b4 = new B(4);
static {
b3 = new B(3);
}
B b5 = new B(5);
{
System.out.println("\t\t非静态代码块2");
}
B b6 = new B(6);
A (int x) {
System.out.println("\tA3、构造方法\n\t\tThis is A" + x);
}
}
class B {
B(int x) {
System.out.println("\t\tThis is B" + x);
}
}
class C {
static {
System.out.println("\tC1、静态变量和静态代码块");
}
{
System.out.println("\tC3、非静态变量和非静态带代码块");
}
static B b7 = new B(7);
public static void func () {
System.out.println("\tC2、静态方法\n\t\t(不会加载非静态变量和非静态带代码块)");
}
C(int x) {
System.out.println("\tC4、构造方法\n\t\tThis is C" + x);
}
}
class D {
static {
System.out.println("\tD1、静态变量和静态代码块");
}
{
System.out.println("\tD3、非静态变量和非静态带代码块");
}
static B b8 = new B(8);
static {
System.out.println("\t\t(静态方法如果没被调用就不会加载)\n\t\t(不会加载非静态变量和非静态带代码块)");
}
public static void func () {
System.out.println("\tD2、静态方法");
}
D(int x) {
System.out.println("\tD4、构造方法\n\t\tThis is D" + x);
}
}
输出:
一、在第一次构造类的实例时加载类(类A)
A1、静态变量和静态代码块
This is B1
This is B2
This is B4
This is B3
A2、非静态变量和非静态代码块
非静态代码块1
This is B5
非静态代码块2
This is B6
A3、构造方法
This is A2
-----MAIN START-----
二、或在第一次访问类的静态方法时构造类(类C)
C1、静态变量和静态代码块
This is B7
C2、静态方法
(不会加载非静态变量和非静态带代码块)
三、或在第一次访问类的静态变量时构造类(类D)
D1、静态变量和静态代码块
This is B8
(静态方法如果没被调用就不会加载)
(不会加载非静态变量和非静态带代码块)
四、再次构造类的实例时不会加载类的静态变量和静态代码块
A2、非静态变量和非静态代码块
非静态代码块1
This is B5
非静态代码块2
This is B6
A3、构造方法
This is A1
C3、非静态变量和非静态带代码块
C4、构造方法
This is C1
D3、非静态变量和非静态带代码块
D4、构造方法
This is D1
五、非静态变量和非静态代码块在访问类的静态方法或静态变量时不会加载
D2、静态方法
-----MAIN END-----
Process finished with exit code 0
访问控制修饰符
例:
- 当两个类在同一包内时:只有
private
修饰的属性和方法不能访问,B 中的f1
。 - 当两个类不在同一包内时:
- 如果不是父类和子类:只有
public
修饰的属性和方法可以访问,C 中的f1
。 - 如果是父类和子类:
子类可以访问父类的,情况比较复杂,protected
和public
修饰的属性和方法protected
的意思是本包可以访问,子类可以继承。- 在子类中创建父类对象不能访问父类的
protected
,D 中的f
。 - 在子类中创建该子类对象能访问父类的
protected
,D 中的f
。 - 在子类中创建另一个子类的对象不能访问公共父类的
protected
,E 中的f
。
- 在子类中创建父类对象不能访问父类的
- 父类只能访问子类的
public
修饰的属性和方法,A 中的f
。
- 如果不是父类和子类:只有
protected static
无论是否在同一包内,子类可以直接访问,不同包的非子类则不可。- 怪事:B 中的
f2
,C 中的f2
,咕咕咕~
// 共四个类:A、B、C、D、E。
// A、B 在同一包内,C、D、E 在同一包内,D、E 是 A 的子类。
// 被注释掉的不能通过编译
// 类 A
package com.wxy;
import com.orz.D;
public class A {
private int privateNum;
int defaultNum;
protected int protectedNum;
public int publicNum;
protected static String s = "Hello";
void f(D d1) { // 不同包内,是父类,只有 public 可以访问
// d1.privateNumD = 1;
// d1.defaultNumD = 1;
// d1.protectedNumD = 1;
d1.publicNumD = 1;
System.out.println(A.s);
System.out.println(d1.s);
}
}
// 类 B
package com.wxy;
import com.orz.D;
public class B {
void f1(A a1) { // 同一包内,只有 private 不能访问
// a1.privateNum = 1;
a1.defaultNum = 1;
a1.protectedNum = 1;
a1.publicNum = 1;
}
void f2(D d1) {
// d1.privateNum = 1;
// d1.defaultNum = 1;
d1.protectedNum = 1;
d1.publicNum = 1;
System.out.println(A.s);
System.out.println(d1.s);
}
}
// 类 C
package com.orz;
import com.wxy.A;
public class C {
void f1(A a1) { // 不同包内,不是父类和子类,只有 public 可以访问
// a1.privateNum = 1;
// a1.defaultNum = 1;
// a1.protectedNum = 1;
a1.publicNum = 1;
}
void f2(D d1) {
// d1.privateNum = 1;
// d1.defaultNum = 1;
// d1.protectedNum = 1;
d1.publicNum = 1;
// System.out.println(A.s);
// System.out.println(d1.s);
}
}
// 类 D
package com.orz;
import com.wxy.A;
public class D extends A {
private int privateNumD;
int defaultNumD;
protected int protectedNumD;
public int publicNumD;
void f(A a1, D d1) { // 不同包内,是子类,protected 和 public 可以访问
// a1.privateNum = 1;
// a1.defaultNum = 1;
// a1.protectedNum = 1;
a1.publicNum = 1; // 子类中创建父类对象不能访问父类的 protected
// super.privateNum = 1;
// super.defaultNum = 1;
super.protectedNum = 1;
super.publicNum = 1;
// privateNum = 2;
// defaultNum = 2;
protectedNum = 2;
publicNum = 2;
// d1.privateNum = 3;
// d1.defaultNum = 3;
d1.protectedNum = 3; // 在子类中创建该子类对象能访问父类的 protected
d1.publicNum = 3;
System.out.println(A.s); // 更推荐这种写法
System.out.println(d1.s);
}
}
// 类 E
package com.orz;
import com.wxy.A;
public class E extends A{
void f(A a1, D d1) {
// super.privateNum = 1;
// super.defaultNum = 1;
super.protectedNum = 1;
super.publicNum = 1;
// privateNum = 2;
// defaultNum = 2;
protectedNum = 2;
publicNum = 2;
// d1.privateNum = 3;
// d1.defaultNum = 3;
// d1.protectedNum = 3; // 在子类中创建另一个子类的对象不能访问公共父类的 protected
d1.publicNum = 3;
System.out.println(A.s);
System.out.println(d1.s);
}
}
重载 覆盖 隐藏
多态分类:
- 静态多态:重载、隐藏
- 动态多态:覆盖 / 重写、抽象方法、接口
重载和覆盖 / 重写:
-
重载:在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同,但是如果其参数列表完全一样,仅仅返回值类型不同,则编译时会产生错误。
// 被注释掉的不能通过编译 public class Test { int f() {return 1;} // void f() {} void f(int a) {} int f(double a) {return 1;} }
-
覆盖 / 重写:
- 子类对父类的允许访问的方法的实现过程进行重新编写。
- 参数列表必须完全相同。
- 返回类型必须是父类返回值的派生类。
- 访问权限不能比父类中被重写的方法的访问权限更低。
- 不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。
- 父类的成员方法只能被它的子类重写,声明为
final
的方法不能被重写,声明为static
的方法不能被重写。
隐藏和覆盖 / 重写:
-
隐藏:属性、私有方法、静态方法。
- 非静态属性、静态属性、静态方法都不能体现多态。
- 子类的静态变量可以隐藏父类的实例变量;子类的实例变量可以隐藏父类的静态变量。
- 不能用子类的静态方法隐藏父类中的实例方法;不能用子类的实例方法覆盖父类中的静态方法。
-
覆盖 / 重写:见上。
public class Test { public static void main(String [] args) { A test = new B(); System.out.println(test.a1); System.out.println(test.a2); test.f1(); // 多态:父类引用指向子类实例,调用子类方法 test.f2(); } } class A { String a1 = "Parent"; static String a2 = "Parent"; void f1() {System.out.println("Parent");} static void f2() {System.out.println("Parent");} } class B extends A{ String a1 = "Child"; // 隐藏 static String a2 = "Child"; // 隐藏 void f1() {System.out.println("Child");} // 覆盖 static void f2() {System.out.println("Parent");} // 隐藏 }
输出:
Parent Parent Child Parent
参考:
抽象类 接口
抽象类:
- 抽象类是不能实例化的,但可以创建它的引用。
- 如果一个类里有抽象的方法,则这个类就必须声 明成抽象的;但一个抽象类中却可以没有抽象方法。
- 如果
extends
抽象的类没有实现接口的全部方法,要被定义成抽象类。
接口:
- 接口不能实例化,没有构造方法。
- 接口中的变量是
public static final
,接口中的方法是public abstract
。 - 如果
implements
接口的类没有实现接口的全部方法,要被定义成抽象类。 - 类在实现接口的方法时,需要显示使用
public
。
例:
-
有如下接口:
interface I0 { void f(); } interface I1 { void f(); } interface I2 { int a = 2; int f(); } interface I3 { int a = 3; int f(int i); } interface I4 { void f(int i); }
-
接口的多继承:
class Test01 implements I0, I1 { @Override void f() { } // 需要添加 public } class Test02 implements I0, I2 { // I0 和 I2 中有参数相同返回类型不同的同名函数,无法重写 @Override public void f() { } @Override public int f() { return 0;} } class Test23 implements I2, I3 { @Override public int f() { return a; } // 需要将 a 修改为 I2.a 或 I3.a @Override public int f(int i) { return i; } }
-
更加复杂的接口的多继承:
class A implements I0 { @Override public void f() { System.out.println("f of A"); } } class B extends A implements I1 { @Override public void f() { System.out.println("f of B"); } } class N extends A implements I2 { // 编译不通过,原因同上例的 Test02 @Override public void f() { } } class C extends A implements I3 { // 可以通过编译,但是要实现 I3 中的方法,否则要声明为抽象类 @Override public void f() { } @Override public int f(int x) { return a; } } class D extends A implements I4 { // 可以通过编译,但是要实现 I4 中的方法,否则要声明为抽象类 @Override public void f() { } @Override public void f(int x) { } } public class Test { public static void test1(I0 i0) { i0.f(); } public static void test2(I1 i1) { i1.f(); } public static void main(String [] args) { A a = new A(); A x = new B(); B b = new B(); Test.test1(a); // 输出 f of A Test.test1(x); // 输出 f of B Test.test1(b); // 输出 f of B Test.test2(a); // 编译不通过 Test.test2(x); // 编译不通过 Test.test2(b); // 输出 f of B } }
内部类 匿名类
实例内部类:
- 在创建实例内部类的实例时,外部类的实例必须已经存在。
- 在内部类中,可以直接访问外部类的所有成员,包括成员变量和成员方法。
- 实例内部类的实例自动持有外部类的实例的引用。
静态内部类:
- 在创建内部类的实例时,不必创建外部类的实例。
- 静态内部类的实例不会自动持有外部类的特定实例的引用。
局部内部类:
- 局部内部类只能在当前方法中使用。
- 局部内部类和实例内部类一样,可以访问外部类的所有成员。
- 局部内部类还可以访问所在方法中用
final
修饰变量或参数。
匿名类:
- 将类和类的方法定义在一个表达式范围里。
- 匿名类本身没有构造方法,但是会调用父 类的构造方法。
例——内部类和匿名类:
public class Test {
public static void main (String [] args) {
A.B x = new A().new B(); // 必须先实例化外部类,再实例化内部类
x.show();
// System.out.println(C.a); // 不能通过编译
System.out.println(C.b);
// System.out.println(C.D.c); // 不能通过编译
System.out.println(C.D.d);
E y = new E();
y.method();
Test anonymous = new Test() { // 匿名类
void show() {
System.out.println("hhh");
}
};
anonymous.show();
}
void show() {
System.out.println("xxx");
}
}
class A {
private int num = 1;
class B { // 实例内部类
public int num = 2;
public void show() {
int num = 3;
// 访问外部类 A 的属性,即使是 private 也能访问
System.out.println(A.this.num); // 1
System.out.println(this.num); // 2
System.out.println(num); // 3
}
}
}
class C {
public int a = 10;
public static int b = 20;
static class D { // 静态内部类
public int c = 30;
public static int d = 40;
public void show() {
// 不能通过编译,静态内部类的实例不会自动持有外部类的特定实例的引用
// System.out.println(a);
}
}
}
class E {
public int a = 100;
public final int b = 200;
public void method() {
int c = 300;
final int d = 400;
c = 301;
class F {
int e = 500;
public void show() {
System.out.println(a);
System.out.println(b);
// System.out.println(c); // 不能通过编译
System.out.println(d);
System.out.println(e);
}
}
new F().show();
}
}