0. 前言
大家好,我是多选参数的程序锅,一个正在 neng 操作系统、学数据结构和算法以及 Java 的硬核菜鸡。下面是本篇的内容提纲:
1. 类
Java 中类的声明形式如下所示,变量的声明和方法的定义意味着只能声明变量、初始化、方法定义等,而不能在方法外进行赋值等操作。
class 类名 {
变量的声明;
方法的定义;
}
Java 中的类名推荐使用大驼峰命名法,也就是首字母大写,然后每个单词都大写,比如 ChinaMade。
1.1. 变量的声明
1.1.1. 实例成员变量
这种是实例成员变量的声明,声明的变量在类内都可以使用。可以声明的类型包括:整型、浮点型、字符型、逻辑类型、数组、对象、接口等。
Java 中的成员变量名字推荐使用小驼峰命名法,也就是首字母小写,后面的每个单词都大写,如 chinaMade。另外一行只声明一个变量。
1.1.2. 类变量
使用 static 关键字声明类变量,如
class Point {
int w; // 实例变量
static int h; // 类变量
}
类变量和实例变量的区别:
- 类变量是多个对象共享的,但是实例变量是每个实例对象私有的,每个对象的实例变量互不相同。也就是说当用 new 创建多个不同的对象时,这些对象是共用一个类变量的,假如此时一个对象改变了这个类变量,那么其他对象中的这个类变量自然也是变了的。
- 类的字节码文件被加载到内存后,类中的类变量也被加载到内存了。
- 类变量的访问可以通过某个对象访问,也可以通过类名访问。
1.2. 方法的定义
1.2.1. 实例方法
返回值类型 方法名(参数列表) {
......
}
int test() {
......
}
void test() { // 不返回任何数据时,使用 void
......
}
方法定义中声明的变量(包括括号内声明的变量和参数列表中的变量)称为局部变量,局部变量具有以下这些性质:
- 只在方法中有效;
- 从声明它的位置之后开始都是有效的;
- 复合语句或循环中的语句声明的变量只在相应的语句中有效;
- 局部变量和成员变量的名字相同的话,则成员变量会被隐藏。想要使用被隐藏的成员变量,可以使用 this 关键字;
- 类变量类似,如果类变量和局部变量相同的话,那么类变量会被隐藏,要想使用隐藏的成员变量,可以使用 ”类名.“ 的方式来调用类变量。
- 成员变量有默认值的,但是局部变量是没有默认值的;
1.2.2. 类方法
同样使用 static 关键字声明类方法,如
class Point {
float max(flaot x, float y) { //实例方法
......
}
static float jerry() { // 类方法
......
}
}
类方法与实例方法的区别:
- 类方法可以通过对象调用,也可以通过类名调用。
- 实例方法中即可访问实例变量,也可以访问类变量。但是类方法不可以操作实例变量,因为在类的字节码被加载时,类方法也会被分配相应的入口地址,但是这时候对象可能还没有创建,实例成员变量还没有分配内存,假如可以访问实例成员变量的话,那么将会出错。
1.2.3. 构造方法
类中的一种特殊方法,创建对象时会调用该类的构造方法。构造方法需要注意以下几点:
-
方法名必须与类名相同;
-
构造方法没有类型;
-
允许存在多个构造方法,但是参数列表要不同(参数的个数不同或者参数的个数相同但参数列表对应位置上的参数类型不同);
-
默认情况下类中都会有一个默认的构造方法,该构造方法是无参数、无实现的。假如不手动编写构造方法,调用的将会是这个默认构造方法;假如编写了构造方法,那么这个默认方法将会失效。
class Point{
int h;
int w;
Point() {
this.h = 1;
this.w = 1;
}
Point(int h, int w){
this.h = h;
this.w = w;
}
public void printAll() {
System.out.println(this.h + " " + this.w);
}
}
1.2.4. 方法重载
一个类中可以有多个同名的方法,但是这些方法的参数列表是不一样的,即参数的个数不同或者参数的个数相同,但是对应位置上的参数类型不同。方法的返回类型和参数的名字不参与比较。
2. 修饰符
2.1. 访问权限
2.1.1. public --- 可修饰类
该成员可以被任意类中的方法访问
2.1.2. private
只有同一个类中的成员方法才能访问私有成员,在其他类中不能直接调用。
2.1.3. protected
介于 private 和 public 之间,①同一个包内的所有类的所有方法都能访问该成员;②如果不在同一个包内的类的方法要访问该成员,则该类必须是该成员所在类的子类。
2.1.4. default --- 可修饰类
同一个包内的类的方法都能访问该成员,可以省略不写。
2.1.5. 总结
权限大小排序:public > protected > default > private
修饰词 | 同一个类 | 同一个包 | 子类(不同包) | 不同包中无继承关系的类 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | |
default | √ | √ | ||
private | √ |
2.2. final 修饰符
-
final 修饰类,表示这个类不能被继承,即不能有子类
-
final 修饰方法,表示这个方法不允许被子类重写
-
final 修饰成员变量或者局部变量,表示这是一个常量,不允许被修改
2.3. abstract 修饰符
- abstract 修饰类表示这是一个抽象类,抽象类更多是用作上转型的对象,即使用 abstract 类的类型声明引用变量,然后把子类对象实例赋值给该引用变量。注意用 abstract 修饰之后不能再用 final 修饰一个类。
- abstract 修饰方法,表示这个方法只允许声明,而没有具体实现。同样注意用 abstract 修饰之后不能再用 final 修饰一个方法,也不能再用 static 修饰。
abstract class Demo { // abstract 类
abstract int min(int x, int y); // abstract 方法
}
abstract 类和方法需要注意以下几点:
- abstract 类可以有 abstract 方法,也可以有非 abstract 方法;而非 abstract 类中不允许有 abstract 方法。因此,当 abstract 的子类不是抽象类的时候,那么必须重写父类所有的 abstract 方法;当 abstract 的子类是抽象类的时候,那么可以重写父类的 abstract 方法也可以选择继承。
- 对于 abstract 类,不能使用 new 来创建该类的对象。因此,abstract 更多是用作上转型的对象。
2.4. static 修饰符
就是上面的类成员变量和类方法。
3. 对象
3.1. 对象的创建 --- new 关键字
new 构造方法
之后,将会在堆区创建一个对象,相当于类的实例,然后会返回堆区的地址(引用)。可以将这个y地址(引用)赋值给某个引用变量。
// 类的名字 引用变量 = new 构造方法
Point p = new Point();
3.2. 对象操作 --- . 符号
引用变量指向某个对象实例之后,那么引用变量可以使用 “.” 来访问对象实例中的内容,比如调用方法、访问成员变量等。
p.printAll()
3.3. 对象数组
对象数组,这个数组中实际上引用变量的数组。
// 创建了一个对象数组,数组中的每一个元素都是引用变量,都可以指向该类型的对象实例。此时创建之后,每个数组元素并没有指向。
Point[] ps = new Point[10];
ps[0] = new Point(); // ps 对象数组中,索引为 0 的数组元素指向了一个对象实例
3.4. this 关键字
this 是指调用该方法或者成员变量的当前的对象实例、构造方法正在创建的对象实例。比如下面这段代码中,当 p 调用 printAll()
方法的时候,该方法中的 this 等同于 p,this 与 p 指向同一个对象实例。Ponit()
这个构造方法中,其实表示将 1、2 赋值给新创建的对象实例。
class Point() {
int h;
int w;
Point() {
this.h = 1;
this.w = 2;
}
void printAll() {
System.out.println(this.h + " " + this.w);
}
}
Point p = new Point();
p.printAll();
this 关键字可以出现在实例方法和构造方法中,但是不能出现在类方法中,这是因为类方法可以通过类名调用,这个时候可能没有对象实例;就算有了,通过类名方式调用的话,this 也不知道指向哪个实例对象。
一个实例方法正在调用类中另一个方法的时可以省略 this 关键字或类名。
4. 面向对象
面向对象的三要素是封装、继承和多态。
4.1. 封装
封装就是将事物抽象为类,把对外接口暴露,将实现和内部数据隐藏。
4.2. 继承
继承是指这样一种能力:它使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展。继承创建的新类称为“子类”或者“派生类”;被继承的类称为“基类”、“父类”或“超类”。实现继承一般使用“继承”或者“组合”来实现,Java 就是使用“继承“来实现的,关键字是 extends。在有些 OOP 语言中子类还可以继承多个基类,但是 Java 中只允许继承一个类。子类继承父类的示例代码如下所示:
class 子类名 extends 父类名 {
......
}
class Student extends People {
......
}
继承需要注意以下几点:
-
子类声明继承父类之后,将继承父类的成员变量或方法,就相当于子类中声明了父类成员变量或方法一样。但是,**子类能继承的成员变量/方法还受到访问权限和包位置的影响,**具体的话分为以下两种情况:
- 子、父类在同一个包:父类的所有都会被继承,包括父类中不是 private 的成员变量/方法;
- 子、父类不在同一个包:private 和 default 的成员变量不会被继承;protected 和 public 访问权限的成员变量/方法可被子类继承;
-
另外,继承的成员变量和方法还是属于父类,子类中只相当于存了这些的引用,实际访问的还是父类中,只是继承的成员变量/方法可以通过子类访问,所以当访问继承的方法时,该方法中访问的成员变量其实是父类的成员变量(因为父类在实现的时候,并不知道自己会被哪些类继承,所以无法知道除自己之外的成员变量,只能操作自己的成员变量)。
比如 B 是 A 的子类,C 是另一个不与 A、B 有任何关系,那么在 C 类中创建了一个 B 类的实例对象,那么在 C 类中访问 B 类自己声明的 protected 变量的话,那么 B 类和 C 类需要放在一个包中;假如 C 类中访问 B 类继承的 protected 变量的话(实际上访问的是 A 类 protected 变量),那么 A 类和 C 类要在同一个包才行。
-
子类不继承父类的构造方法。
4.2.1. 成员变量的隐藏
当子类声明的成员变量的名字和从父类那边继承来的成员变量的名字相同时,那么子类就会隐藏继承的成员变量。那么,子类自己定义的方法可以操作子类继承的成员变量和子类自己生命的变量,但无法直接访问子类隐藏的成员变量;子类继承的方法操作的是子类继承和隐藏的成员变量,也就是父类自己的成员变量。示例代码如下所示,子类继承 method 方法,那么 method 操作的是父类 A 中 a、b。
public class A {
int a;
int b;
public int method() {
return a*b;
}
}
public SubA extends A {
int a;
int b;
public int methodSub() {
return a + b; // 是指 SubA 中的 a、b
}
}
4.2.2. 方法重写
同成员变量类似,子类继承父类的某个方法之后,子类有权利去重写这个方法。重写是指,子类中重新定义了一个方法,这个方法的返回值类型、名字、参数列表(参数的个数、参数的类型)都跟父类的方法完全相同(返回值类型不相同的话也行,但是需要确保重写之后的类型是父类方法类型的子类型)。一旦重写,那么继承的父类方法将被隐藏起来。重写的示例代码如下所示:
public class Demo {
public float computer (float x, float y) {
return x + y;
}
}
public class SubDemo extends Demo {
public float computer (float x, float y) {
return x * y;
}
}
重写需要注意以下几点:
- 也就是你要准备重写了的话,那么上述需要相同的内容都得确保相同,假如返回值类型相同、名字相同、参数列表不相同,那么这种是方法重载了。假如名字相同、参数列表相同,返回值类型不同了,那么这个是不允许。
- 重写方法的时候,不允许降低方法的访问权限,但是可以提高访问权限。
4.2.3. super 关键字
super 关键字主要是用来操作被隐藏的成员变量/成员方法和构造方法。
-
操作隐藏的成员变量/成员方法
如果子类中想使用被子类隐藏的成员变量或者方法,那么使用 super 关键字即可。比如
super.x; super.play();
-
调用父类的构造方法
子类的构造方法在编写的时候一定先调用父类的某个构造方法。当然,默认情况下已经相当于调用了父类不带参数的构造方法,即
super()
,所以都可以不用再写。假如自己要调用父类的某个构造方法的话,那么一定要把调用放在构造方法的第一句。因为,子类的构造方法在默认情况下会调用父类不带参数的构造方法,因此在实现类的时候,如果实现了带参数的构造方法,那么一定要添加一个无参数的构造方法,以防子类出错。
4.2.4. 子类与对象
使用子类的构造方法创建一个子类的对象时,子类和父类的成员变量都分配了内存空间,其实通过上述 super 可以看到,父类的构造方法也被调用了,因此相当父类也是被创建了的。
下面来阐述一下 instanceof 运算符,这是一个二元运算符,左边是一个引用变量,右边是一个类,主要是判断左边引用变量所指的实例对象是否是右边类的一个实例对象,如下所示,输出为 True。这是因为 sp 指向的还是 new Student() 出来的实例对象。
Student stu = new Student()
SchoolPeople sp = stu;
if (sp instanceof Student) {
System.out.println("True!");
} else {
System.out.println("False!");
}
================================
True
4.3. 多态
多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它子对象的特性以不同的方式运作。简单的说,就是把子类的对象实例赋值给父类的引用变量,父类的引用变量就可以访问子类的成员变量或者方法,因为赋值的子类对象实例不同,因此呈现多态。将子类的引用变量赋值给父类引用之后,父类引用变量称为子类引用变量的上转型对象,如下所示,a 被称为 b 的上转型对象。
// Tiger 是 Animal 的子类
Animal a;
Tiger b = new Tiger();
a = b;
// 上述等价于:Animal a = new Tiger();
那么上转型对象需要注意以下几点:
- 上转型对象不能操作子类新增的成员变量或方法。
- 上转型对象可以访问子类继承或隐藏的成员变量,调用子类继承的方法或子类重写的方法(就相当于子类对象调用这些方法)。
- 如果子类重写了父类的静态方法,那么子类对象的上转型对象不能调用子类重写的静态方法,只能调用父类的静态方法。
这几点总的来说就是上转型对象中指向的是子类对象实例,但是引用变量的类型还是父类的,所以只能访问父类中有的内容,比如子类继承或隐藏的成员变量,子类继承的方法或子类重写的方法,这些父类中都有。另外,为什么静态方法需要那样呢?因为静态方法可以通过类名调用的,静态方法是属于这个类的方法,所以当你使用上转型对象调用静态方法时相当于父类去调用静态方法,那调用的自然是父类的静态方法。
那么怎么理解多态呢?个人理解就是,相同方法,在父子类中展现不同行为特征的能力,这个主要是因为赋值给父类引用变量的子类对象实例不同而呈现不同。那么怎么可以让相同方法的方法被调用时可以不同呢?那就是让子类重写父类中的某个方法(static 方法除外)。 如下所示,那么当把 Teacher 的实例对象传递给 sp 的时候,sp.work()
调用的是 Teacher 这个类中的 work()
方法,输出的是 Teacher Work
;当把 Student 的实例对象传递给 sp 的时候,sp.work()
调用的是 Student 这个类中的 work()
方法,输出的是 Student Work
,也就呈现多态的特性。需要注意的是,sp.joinClub()
是错误的,因为 SchoolPeople 中并没有这个方法,假如有一次传给 sp 的是 Teacher 实例对象,但是 Teacher 类中并没有 joinClub()
这个方法,那这样调用的话就崩溃了。
public class SchoolPeople {
public void work() {
System.out.println("SchoolPeople Work");
}
}
public class Student extends SchoolPeople {
public void work() {
System.out.println("Student Work");
}
public void joinClub() {
System.out.println("Join Club");
}
}
public class Teacher extends SchoolPeople {
public void work() {
System.out.println("Teacher Work");
}
public void attendMeeting() {
System.out.println("Attend Meeting");
}
}
public class MainClass {
public void manager(SchoolPeople sp) {
sp.work();
}
}
其实还有一个向下转型,就是把父类引用变量指向的实例对象转化为或者赋值给相应子类的引用变量,这个时候是需要强制类型转换的。比如下面的代码:
// Student 是 SchoolPeople 的子类 Student stu = new Student(); SchoolPeople sp = stu Student stu1 = (Student)sp;
当然需要注意,当向下转型时,父类引用变量指向的实例对象的对象类型要与赋值的子类引用变量的类型是一致的,下面这样的代码就是不对了的:
// Cat、Dog 是 Pet 的子类 Cat cat = new Cat(); Pet pet = cat; Dog dog = (Dog)pet; // 这就错了
5. 内部类
5.1. 实名内部类
实名内部类是指在类中再嵌套一个类的定义。内部类的修饰词可以是 public、protected、default、private;并且内部类可以访问外嵌类的成员变量和方法。这边我将内部类分为两类:非静态实名内部类和静态实名内部类。
5.1.1. 非静态实名内部类
非静态实名内部类其实也就是没有 static 关键字修饰的内部类,那么这个类类似于一个成员变量。在内部类中需要注意以下几点:
-
如果成员域具有 static 属性,那么必须要有 final 属性,即 final static;
-
不能含有 static 属性的成员方法;
-
创建该内部类的对象实例时,需要先创建外部类的对象,然后通过外部类的引用变量创建内部类的对象。如下
public class Test { public static void main(String[] args) { OutClass out = new OutClass(); OutClass.InnerClass inner = out.new InnerClass(); } } public class OutClass { class InnerClass { final static int value = 4; } }
-
成员的访问:对于静态成员,则使用
外部类名.内部类名.成员名
;对于非静态成员,引用变量(指向内部类的对象实例).成员名
5.1.2. 静态实名内部类
静态实名内部类也就是有 static 关键字修饰的内部类,类似于类成员变量。在静态实名内部类中,需要注意以下几点:
-
static 内部类不能操作外嵌类的实例成员变量(可以想想类方法,static 内部类在二进制文件被加载的时候就已经分配了,然而此时实例成员变量可能还分配内存等);
-
相比非静态实名内部类来说,静态实名内部类可以有 static 方法。那么,也就说明静态实名内部类,其实跟普通类一样,只是需要注意第一点。
-
创建静态内部类的对象实例时,采用如下方法:
public class Test { public static void main(String[] args) { OutClass.InnerClass inner = new OutClass.InnerClass(); } } public class OutClass { static class InnerClass { final static int value = 4; } }
-
成员的访问:对于静态成员,则使用
外部类名.内部类名.成员名
;对于非静态成员,引用变量(指向内部类的对象实例).成员名
5.2. 匿名内部类
匿名内部类是没有类名,在 Java 中经常被用到。匿名内部类如下所示,表示定义了一个没有名字的子类,并同时创建该子类的一个实例对象。
new 父类名(父类构造方法参数列表) {
子类自己的实现;
}
- 父类名也可以是接口名,只是假如是接口的话则不能有参数列表;
- 子类实现中可以继承父类的方法也可以重写;
- 父类有抽象方法的话,子类的实现中必须实现抽象方法;
- 不能具有抽象方法或属性,不能有 static 属性的成员变量和方法(假如成员变量具有 static 属性,那么必须要有 final 属性);
- 匿名类实例对象的方法,通过它父类型的引用变量来访问。常用在图形用户界面设计中,进行各种事件的处理。
6. 接口
使用 interface 来定义一个接口,如下所示
interface Printable {
int MAX = 100;
void add();
float sum(float x, float y);
}
- 接口中只能包含常量的声明,而没有变量,所有常量都是
public final static
,为了方便这三个修饰符在接口中都可以省略不写。 - 接口中只有抽象方法,没有普通的方法,所有的抽象方法都是
public abstract
,为了方便这两个修饰符在接口中都可以省略不写。
6.1. 实现接口
一个类使用 implements 关键字表示实现了某个接口,如
class Demo implements Printable {}
- 一个类可以实现多个接口,如
class Demo implements Printable, Addable {}
- 父类实现了某接口,子类自然也是实现了该接口,因此子类中不必再显示地使用 implements 声明实现了这个接口。并且子类继承的是父类中已重写的接口方法和其他方法。
- 接口可以有自己的子接口,用于类的那些修饰符也可以修饰接口,并且效果一样。
- 由于接口中的方法都是抽象方法,而且是 public。因此,当一个非抽象类实现了某个接口之后,那么这个类必须重写这个接口中的所有方法,并且重写的方法的访问权限都得是 public;假如是一个抽象类实现了某个接口,那么由于抽象类中可以包含抽象方法,因此抽象类既可以重写接口中的方法,也可以直接拥有接口中的方法。
6.2. 接口回调
首先,阐述一下什么是接口变量?接口变量也就是用接口类型声明的变量。可以将实现了该接口的类的对象实例的引用赋值给该接口变量。如下
interface Com {
......
}
class ImpleCom implements Com {
......
}
Com com = new ImpleCom();
ImpleCom obj = new ImpleCom();
Com com = obj;
接口回调就是指可以通过接口变量调用被类实现的接口的方法,但是类中其他的非接口方法是无法通过接口变量调用的。
7. Package
Java 中的 package 类似于 C 语言中的头文件的概念。在 Java 中将相似功能的类都放在同一个包中,该包的包名就是该类所在的目录路径,比如这个类所在的目录路径是 com/edu/cn
,那么包名就应该是 com.edu.cn
。
7.1. package 和 import 关键字
Java 使用 package 关键字显式得将一个类放入到某个包中,如在一个类的开头使用 package com.edu.cn
则表示将这个类放到 com.edu.cn
这个包中,com.edu.cn
其实是一个逻辑路径,表示该类位于逻辑路径 com/edu/cn
中,为了 import 的正确性,还需确保这个逻辑路径要跟类所在的物理路径一样。那么,当一个类有了包名之后,那么这个时候这个类的全称应该是 包名.类名
。
package com.edu.cn
public class DemoClass {
......
}
当你想要导入一个包中的某个类的时候,使用 import 关键字,import ******
。比如该类存放在物理路径 com/edu/cn
中,该类所处的逻辑路径也应该是 com/edu/cm
,即类所处包名应该是 com.edu.cn
,那么则使用 import com.edu.cn.类名
即可正确导入该类。这个时候必须确保 import 导入的类所在的物理路径和类的逻辑路径是相同,这是因为 import 将会根据包名去相应的物理路径中寻找这个类(全称),比如 import com.edu.cn.A
,则会去 com/edu/cn
目录中寻找 com.edu.cn.A
这个类文件。但是,假如包名和类所处的物理路径不一致,将会无法正确加载。
下面我们来看一个例子,有两个类 DemoClass1 和 DemoClass2,这两个类所在的 java 文件位于同一物理路径下,DemoClass2 会用到 DemoClass1,那么 DemoClass2 会去 classpath 设置的环境变量中寻找叫 DemoClass1 的类,但是当前目录下的类不叫 DemoClass1,而是叫 com.edu.cn.DemoClass1,所以将无法正确加载。
// DemoClass1 类
package com.edu.cn
public class DemoClass1 {
......
}
// DemoClass2 类
public class DemoClass2 {
public static void main(String args[]) {
DemoClass1 demo = new DemoClass1();
}
}
可以将 DemoClass2 也 package 到 com.edu.cn 中,那么可以正确加载
package com.edu.cn
public class DemoClass2 {
public static void main(String args[]) {
DemoClass1 demo = new DemoClass1();
}
}
7.2. 导入机制
Java 中有两种包的导入机制:
- 单类型导入(single-type-import),例如
import java.io.File
。单类型导入是仅仅导入一个 public 类或者接口。 - 按需类型导入(type-import-on-demand),例如
import java.io.*
。按需导入不是导入一个包下的所有类,而是导入当前类需要使用的类。
单类型导入和按需类型导入对类文件的定位算法是不一样的。Java 编译器会从启动目录,扩展目录和用户类路径下去定位需要导入的类,而这些目录/路径仅仅给出了类的顶层目录。编译器的类文件定位方法大致可以理解为如下公式:顶层路径名/包名/文件名.class == 绝对路径
。
-
对于单类型导入很简单,因为包名和文件名都已经确定,所以可以一次性查找定位。
-
对于按需类型导入则比较复杂,编译器会把包名和文件名进行排列组合,然后对所有的可能性进行类文件查找定位。如下面代码所示:
package com; import java.io.*; import java.util.*;
当你的类文件中用到了 File 类,那么可能 File 类的地方如下:
- File。这个 File 类属于无名包,就是说 File 类 没有 package 语句,编译器会首先搜索无名包
- com.File。File 类属于当前包
- java.lang.File。编译器会自动导入 java.lang 包
- java.io.File。
- java.util.File
然而编译器找到 java.io.File 类之后并不会停止下一步的寻找,而是把所有的可能性都查找完以确定是否有类导入冲突。假设此时的顶层路径有三个,那么可能所处的位置会有 3*5=15 处,也就是 15 次查找。如果,查找完成后,编译器发现了两个同名的类(个人认为不是类的全称),那么就会报错。