目录
一、访问控制符
1. 修饰符
→ 修饰符(modifiers)分为两类,可以修饰类及其成员(字段、方法)
① 访问修饰符:public,private 等等
② 其他修饰符:abstract
2. 成员的访问控制符(权限修饰符)
在同一个类中 | 在同一个包中 | 不同包中的子类 | 不同包中的非子类 | |
---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ |
默认(包可访问) | ✅ | ✅ | ❌ | ❌ |
protected | ✅ | ✅ | ✅ | ❌ |
public | ✅ | ✅ | ✅ | ✅ |
【举例】下面的例子中文件结构如下:
tree
.
├── Derived1.java
├── Main.java
└── pkg
├── Test.java
└── pkg1
└── Test1.java
2 directories, 4 files
为清晰起见,绘制 UML 图如下(图中有些类,如 Group 将在后面的笔记中出现):
代码及注释如下:
→ Main.java 中的 Base 类和 Derived 类:
class Base {
// 不能写 public class Base 因为文件名不是 base.java
private int a = 1;
int b = 2;
protected int c = 3;
public int d = 4;
Base() {
System.out.println("Base initialized.");
System.out.println("this.a = " + this.a); // 同一个类中可以访问的字段
}
}
class Derived extends Base {
Derived() {
System.out.println("Derived initialized.");
// System.out.println(a); [错误]
System.out.println(b); // 同一个包中可以访问的字段
System.out.println(c);
System.out.println(d);
}
}
→ Main.java 中的 Main 类:
public class Main {
public static void main(String[] args) {
Base base = new Base();
// System.out.println(base.a); [错误]
System.out.println(base.b);
System.out.println(base.c);
System.out.println(base.d);
Derived derived = new Derived();
System.out.println(derived);
Derived1 derived1 = new Derived1();
System.out.println(derived1.e); // 同包的非子类可以访问的字段
System.out.println(derived1.f); // 同包的非子类可以访问的字段
Person person = new Person();
// System.out.println(person.age); [错误]
System.out.println(person.getAge());
Person person1 = new Person(20);
System.out.println(person1.getAge());
if (person.setAge(18)) {
System.out.println("Success!");
} else {
System.out.println("Failed!");
}
System.out.println(person.getAge());
if (person1.setAge(300)) {
System.out.println("Success!");
} else {
System.out.println("Failed!");
}
System.out.println(person1.getAge());
}
}
→ 与 Base 类在同一个包中的 Derived1 类:
public class Derived1 extends Base {
public int e = 10;
int f = 12;
Derived1() {
System.out.println(b); // 同包的子类中可以访问的字段
System.out.println(c); // 同包的子类中可以访问的字段
}
}
→ pkg/Test1.java 中的 Test1 和 Test2 类:
package pkg.pkg1;
import pkg.Test;
public class Test1 extends Test {
public Test1() {
super();
System.out.println("test1");
// System.out.println(pri); [错误]
// System.out.println(def); [错误]
System.out.println(pro);
System.out.println(pub);
}
}
class Test2 {
public Test2() {
Test test = new Test();
System.out.println("test2");
// System.out.println(test.pri); [错误]
// System.out.println(test.def); [错误]
// System.out.println(test.pro); [错误]
System.out.println(test.pub);
}
}
3. 类的访问控制符
→ 定义类时,类的访问控制符要么是 public,要么是默认
① public:该类可以被其他类所访问
② 默认:只能被同包中的类访问
【注意】访问受两层控制:类与字段、方法
4. setter 和 getter 及其优势
① 将字段用 private 修饰,从而更好地将信息封装和隐藏
② set 和 get 都用 public 来修饰,分别用 set 和 get 方法对类的属性进行存取
③ setter 和 getter 的优势:
→ 属性用 private 更好地封装和隐藏,外部类不能随意存取和修改
→ 提供方法来存取对象的属性,在方法中可以对给定参数的合法性进行检验
→ 方法可以用来给出计算后的值
→ 方法可以完成其他必要的工作(如清理资源,设定状态等等)
→ 只提供 get 方法,而不提供 set 方法的字段,可以保证其只读的属性
【举例】Java 中的 setter 和 getter
class Person {
private int age;
Person() {
age = 18;
}
Person(int age) {
this.age = age;
}
public boolean setAge(int age) {
if (age > 0 && age < 200) {
this.age = age;
return true;
}
return false;
}
public int getAge() {
return age;
}
}
二、其他修饰符
1. 三种其他修饰符的含义及应用对象
→ 其他修饰符也即非访问控制符
基本含义 | 修饰类 | 修饰成员 | 修饰局部变量 | |
---|---|---|---|---|
static | 静态的、非实例的、类的 | 可以修饰内部类 | ✅ | ❌ |
final | 最终的、不可改变的 | ✅ | ✅ | ✅ |
abstract | 抽象的、不可实例化的 | ✅ | ✅ | ❌ |
2. static 的使用
① static 字段(在一定意义上,可以表示全局变量)
→ static 成员不属于任何一个对象的实例,保存在类的内存区域的公共存储单元
→ 类变量可以通过类名直接进行访问,也可以通过实例对象来访问,结果相同
【举例】System 类的 in 和 out 对象属于类的域,可用类名直接访问,即 System.in 和 System.out
② static 方法
→ 静态方法(类方法)属于整个类
→ 静态方法不能操作和处理属于某对象的域,只能处理本类中的静态域或调用静态方法
→ 静态方法中,不能访问实例变量,不能使用 this 或 super
→ 调用这个方法时,应该是用类名直接调用,也可以用某一个具体的对象名
【举例】static 字段及方法的例子如下:
class Group {
static long totalNum; // 在一定意义上,可以表示全局变量
static String location = "Shandong";
int id;
String name;
Group() {
totalNum = 11;
id = 10;
name = "Group 1";
}
static void func() {
// 静态方法(类方法)——属于整个类;调用时应该直接用类,也可以用实例来调用
System.out.println(totalNum);
// System.out.println(id); [错误] 不能操作和处理属于某个对象的成员变量
// func1(); [错误] 只能处理本类中的 static 域或 调用 static 方法
printLocation();
// System.out.println(this.id); [错误] 不能使用 this 或 super
Group group = new Group();
System.out.println(group.id); // [正确]
}
static void printLocation() {
System.out.println(location);
}
void func1() {
// 实例方法——属于某个实例
System.out.println(id);
}
}
③ import static(JDK 1.5 之后可以使用)
→ import static java.lang.System.*; 引入之后,可以直接使用 out.println();
【注意】要求类的所有成员都是 static 成员
3. final 的使用
① final 类与 final 方法
→ 被 final 修饰和限定的类:不能被继承,也就是说不能有子类,其方法不能被 Override,编译器可以实现优化
→ 被 final 修饰和限定的方法:不能被子类覆盖
② final 字段与 final 局部变量(方法中的变量)
→ 值一旦给定,就不能被修改;是只读量,能且只能被赋值一次,不能被赋值多次
→ 字段用 static final 修饰:可以表示常量,例如 Integer.MAX_VALUE,Math.PI 等
→ 与 final 字段、局部变量有关的赋值:
1. 定义 static final 域时,没有给定初始值:按照默认值初始化,数值为 0;boolean 为 false,引用为 null
2. 定义 final 域(非 static)时,必须且只能赋值一次,不能缺省;可以在定义变量的时候赋值,也可以在每一个构造函数中进行赋值
3. 定义 final 局部变量,必须且只能赋值一次;它的值可能不是常量,但它的取值在变量存在期间不会改变
【举例】final 关键字的使用
final class TestFinal {
final int a = 32;
final int b;
final static double PI = 3.14159265; // 也可以写成 final static
TestFinal(int b) {
System.out.println(PI);
// PI = 1.1; [错误]
this.b = b;
// this.b = 10; [错误]
final float c = (float) PI;
System.out.println(c);
// c = 10; [错误]
int d = 20;
final int e = d;
System.out.println(e); // e 的值在变量存在期间不能改变
System.out.println(++d);
}
}
4. abstract 的使用
① abstract 类:抽象类,不能用 new 来实例化,更高度的抽象
② abstract 方法:
→ 为所有子类定义统一的接口,抽象方法只需要声明,无需实现,用分号而不用花括号
→ 抽象类中类包含抽象方法,也可不包含抽象方法
【注意】但若包含抽象方法,必须声明为抽象类
→ 抽象方法在子类中必须被实现,否则子类仍然为抽象类
【举例】abstract 关键字的使用
abstract class TestAbstract {
// 一个抽象类一定不能用 final 来修饰
final int a;
TestAbstract() {
this.a = 7;
System.out.println("TestAbstract initialized.");
}
abstract void func(int x, int y, int z);
static void test() {
System.out.println("TestAbstract: test");
}
}
class TestAbstractDerived extends TestAbstract {
final int b;
TestAbstractDerived(int b) {
super();
this.b = b;
}
@Override
void func(int x, int y, int z) {
System.out.println("TestAbstractDerived: func");
System.out.printf("%d %d %d\n", x, y, z);
}
}
class Test {
public static void main(String[] args) {
Group group = new Group();
System.out.println(group.id);
Group.func();
group.func1();
TestFinal testFinal = new TestFinal(19);
System.out.println(testFinal.a + " " + testFinal.b);
// TestAbstract testAbstract = new TestAbstract(); [错误]
TestAbstract.test();
TestAbstractDerived testAbstractDerived = new TestAbstractDerived(-8);
System.out.println(testAbstractDerived.b);
testAbstractDerived.func(9, 9, 6);
testAbstractDerived.func(1, 2, 1);
TestAbstract testAbstract = new TestAbstractDerived(10);
testAbstract.func(7, 2, 1);
}
}
三、接口
1. 接口的概念
→ 接口是某种特征的约定(抽象)
① 定义接口 interface:所有方法都是 public abstract
② 实现接口 implements:可以实现多继承,与类的继承关系无关
→ 面向接口的编程,不是面向实现;Java 中有大量的接口
Flyable f = new Bird() // 只要能飞就可以啦 ~
→ 接口是引用类型(引用类型主要有三种 class interface 数组)
2. 接口的作用
① 可以实现不相关类的相同行为,不需要考虑这些类之间的层次关系,从而在一定意义上实现了多重继承(一个类里面实现多个不同的特征)
② 可以指明多个类需要实现的方法(更加抽象,类似于 C++ 的纯虚的类)
③ 可以了解对象的的交互界面,不需要了解对象对应的类(见 Main 中解释)3. 接口的定义
3. 接口的定义与类型
【举例】接口的示例
// public 修饰的接口——任意类都可以使用
// 缺省情况下——只有与该接口定义在同一包的类才可以访问
interface Collection {
// 接口 [extends listOfSuperInterfaces] 可以有多个父接口
// 子接口继承父接口所有的常量和方法
// 类 [extends Base] 只能继承一个父类
// 命名往往用 -able 或 -ible 结尾
void add(Object obj);
void delete(Object obj);
Object find(Object obj);
int size();
// 这里默认是 public 但在实现的时候要写上 public
}
class FIFOQueue implements Collection {
// 必须全都实现才行
@Override
public void add(Object obj) {
System.out.println("FIFOQueue: add");
}
@Override
public void delete(Object obj) {
System.out.println("FIFOQueue: delete");
}
@Override
public Object find(Object obj) {
System.out.println("FIFOQueue: find");
return null;
}
@Override
public int size() {
System.out.println("FIFOQueue: size");
return 0;
}
}
public class Main {
public static void main(String[] args) {
// 接口可以作为引用类型来使用
// 任何实现该接口的类的实例都可以存储在该接口类型的变量中
// 通过这些变量可以访问类所实现的接口中的方法
// Java 运行时系统动态地确定使用哪个类的方法
Collection collection = new FIFOQueue();
collection.add(null); // 使用时不需要了解对象对应的类
FIFOQueue fifoQueue = new FIFOQueue();
fifoQueue.size();
System.out.println("Hello world!");
}
}
4. 枚举(从 JDK 1.5 起)
【举例】枚举类型及其使用
enum Light {
// JDK 1.5 之后 可以使用枚举 Java 中的枚举是用类来实现的,可以复杂地使用
Red, Yellow, Green
}
Light light = Light.Red;
switch (light) {
case Red -> System.out.println("Red");
case Yellow -> System.out.println("Yellow");
case Green -> System.out.println("Green");
// 这样写不用 break 哦
}
5. 接口中的常量
【举例】Java 接口中的常量
// 接口中的常量——接口体可以包含常量的定义
// 常量的定义格式为 type NAME = value;
interface Printer1 {
float VERSION1 = 2022.2f;
// 类似于 C++ 中的 const 通常具有 public static final 的属性
default void printInt(int n) {
// 默认方法 Java 8 之后
System.out.println("Printer1: The integer is " + n + ".");
}
default void test() {
System.out.println("Printer1: test");
}
}
6. Java 8 中的接口
【举例】Java 8 中的接口,扩展了传统的抽象概念
interface Printer2 {
float VERSION2 = 2022.4f;
// Java 8 接口成员还可以是 static 方法
// 具有实现体的方法——default 方法
// 好处——提供了一个默认的实现,子类在 implements 中可以不用再写了
static String describe() {
return "Printer2: describe";
}
default void printInt(int n) {
// 默认方法 Java 8 之后
System.out.println("Printer2: The integer is " + n + ".");
}
/* 产生二义性(Printer 将会 extends 两个同名的方法)
default void test() {
System.out.println("Printer2: test");
} */
}
interface Printer extends Printer1, Printer2 {
int INFO = 2022;
void print();
@Override
default void printInt(int n) {
System.out.println("Printer: The integer is " + n + ".");
}
}
class TestPrinter implements Printer {
@Override
public void print() {
System.out.println(INFO);
}
@Override
public void printInt(int n) {
System.out.println("TestPrinter: The integer is " + n + ".");
}
}
class TestPrinter1 implements Printer1 {
@Override
public void test() {
System.out.println("TestPrinter1: test");
}
}
class TestPrinter1_1 implements Printer1 {
}
class TestPrinter2 implements Printer2 {
}
class Test {
public static void main(String[] args) {
Printer printer = new TestPrinter();
printer.print();
printer.printInt(99);
// 默认方法不是抽象方法,不强制被重写
System.out.println(printer.VERSION1 + " " + printer.VERSION2);
System.out.println(Printer2.describe());
// System.out.println(TestPrinter.describe());
// [注意] 接口静态方法只能通过接口名调用,不能通过实现类名或者对象名调用
// [注意] public 可以省略,但是 static 不能省略
TestPrinter1 testPrinter1 = new TestPrinter1();
testPrinter1.printInt(7);
testPrinter1.test();
Printer1 testPrinter1$1 = new TestPrinter1();
testPrinter1$1.printInt(7);
testPrinter1$1.test(); // 动态绑定 输出 TestPrinter1: test
TestPrinter1_1 testPrinter1_1 = new TestPrinter1_1();
testPrinter1_1.test();
TestPrinter2 testPrinter2 = new TestPrinter2();
testPrinter2.printInt(7);
}
}
四、语法小结
1. 完整的类的定义
[public] [abstract | final] class className
[extends superClassName] [implements InterfaceNameList] {
[public | protected | private] [static] [final] [transient] [volatile] type variableName;
[public | protected | private] [static] [final | abstract] [native] [synchronized] returnType methodName([parameterList]) [throws exceptionList] { statements }
}
2. 完整的接口的定义
[public] interface InterfaceName [extends superInterfaceList] {
type constantName = value;
returnType methodName([parameterList]);
还可以有 static 方法 和 default 方法
}
3. 三种要求固定声明方式的方法
① 构造方法:className([parameterList]) {}
② main 方法:public static void main(String[] args) {}
③ finalize 方法:protected void finalize() throws throwable {}
4. 完整的 Java 源文件
① 指定文件中的类所在的包,0个或1个:package packageName;
② 指定引入的类,0个或多个:import packageName[.className | *];
③ 属性为 public 的类的定义,0个或1个:public classDefinition
④ 接口或类的定义,0个或多个:interfaceDefinition & classDefinition
【注意】源文件的名字必须与属性为 public 的类的名字完全相同
→ 在一个 java 文件中,package 语句和 public 类最多只能有一个
→ public 接口的名称需要与文件名相同
【举例】一个完整的 Java 源文件
package pkg;
class Test {
public int a;
// [1] 构造方法——className([parameterList]) {}
Test(int a) {
this.a = a;
}
protected void finalize() {
// [3] 析构方法——编译器使用的方法,编程的时候不写出来
// Overrides method that is deprecated and marked for removal in 'java.lang.Object'
}
}
interface TestInterface {
void func(int a);
}
class TestInterfaceImplementation implements TestInterface {
@Override
public void func(int a) {
System.out.println(a * a);
}
}
public class Main {
public static void main(String[] args) {
// [2] main 方法——public static void main(String[] args) { ... }
Test test = new Test(20);
System.out.println(test.a);
TestInterface testInterface = new TestInterfaceImplementation();
testInterface.func(7);
}
}