本文是参考廖雪峰的官方文档以及黑马57期的视频,记录一些Java快速入门的知识,方便以后复习。
(未完待续)
第七章 面向对象编程
哈哈,老师好可爱呀,面向对象编程,你得首先有个对象,理解并掌握面向对象的基本思想,但不保证能找到对象~
面向对象基础
定义class
在Java中,创建一个类,例如,给这个类命名为Person
,就是定义一个class
:
class Person {
public String name;
public int age;
}
一个class可以包含多个字段(field
),字段用来描述一个类的特征。上面的Person
类定义了两个字段,一个是String
类型的字段,命名为name
,一个是int
类型的字段,命名为age
。因此,通过class
,把一组数据汇集到一个对象上,实现了数据封装。
创建实例
new操作符可以创建一个实例,然后,我们需要定义一个引用类型的变量来指向这个实例:
Person ming = new Person();
上述代码创建了一个Person类型的实例,并通过变量ming指向它。
注意区分Person ming是定义Person类型的变量ming,而new Person()是创建Person实例。
可以通过这个变量来操作实例。访问实例变量可以用变量.字段,例如:
ming.name = "Xiao Ming"; // 对字段name赋值
ming.age = 12; // 对字段age赋值
System.out.println(ming.name); // 访问字段name
Xiao Ming
小结:
(1)在OOP中,class
和instance
是“模版”和“实例”的关系;
(2)定义class
就是定义了一种数据类型,对应的instance
是这种数据类型的实例;
(3)class
定义的field
,在每个instance
都会拥有各自的field
,且互不干扰;
(4)通过new
操作符创建新的instance
,然后用变量指向它,即可通过变量来引用这个instance
;
(5)访问实例字段的方法是变量名.字段名
;
(6)指向instance
的变量都是引用变量。
方法
把field
从public
改成private
,外部代码不能访问这些field
,使用方法(method
)来让外部代码可以间接修改field
:
public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setName("Xiao Ming"); // 设置name
ming.setAge(12); // 设置age
System.out.println(ming.getName() + ", " + ming.getAge());
}
}
class Person {
private String name;
private int age;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
if (age < 0 || age > 100) {
throw new IllegalArgumentException("invalid age value");
}
this.age = age;
}
}
Xiao Ming, 12
虽然外部代码不能直接修改private
字段,但是,外部代码可以调用方法setName()
和setAge()
来间接修改private
字段。在方法内部,我们就有机会检查参数对不对。比如,setAge()
就会检查传入的参数,参数超出了范围,直接报错。这样,外部代码就没有任何机会把age
设置成不合理的值。
所以,一个类通过定义方法,就可以给外部代码暴露一些操作的接口,同时,内部自己保证逻辑一致性。
调用方法的语法是实例变量.方法名(参数);
。一个方法调用就是一个语句,所以不要忘了在末尾加;
。例如:ming.setName("Xiao Ming");
。
定义方法
修饰符 方法返回类型 方法名(方法参数列表) {
若干方法语句;
return 方法返回值;
}
方法返回值通过return
语句实现,如果没有返回值,返回类型设置为void
,可以省略return
。
private方法
private
方法不允许外部调用,定义private
方法的理由是内部方法是可以调用private
方法的。
public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setBirth(2008);
System.out.println(ming.getAge());
}
}
class Person {
private String name;
private int birth;
public void setBirth(int birth) {
this.birth = birth;
}
public int getAge() {
return calcAge(2019); // 调用private方法
}
// private方法:
private int calcAge(int currentYear) {
return currentYear - this.birth;
}
}
11
calcAge()
是一个private
方法,外部代码无法调用,但是,内部方法getAge()
可以调用它。
此外说明方法可以封装一个类的对外接口,调用方不需要知道也不关心Person
实例在内部到底有没有age
字段。
this变量
在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。因此,通过this.field
就可以访问当前实例的字段。
如果没有命名冲突,可以省略this
,如果有局部变量和字段重名,那么局部变量优先级更高,则加上this
。例如:
class Person {
private String name;
public String getName() {
return name; // 相当于this.name
}
}
public void setName(String name) {
this.name = name; // 前面的this不可少,少了就变成局部变量name了
}
}
方法参数
方法可以包含0个或任意个参数。方法参数用于接收传递给方法的变量值。调用方法时,必须严格按照参数的定义一一传递。
可变参数
可变参数用类型...
定义,可变参数相当于数组类型:
public class Main {
public static void main(String[] args) {
City c = new City();
c.setNames("shanghai", "guangzhou"); // 传入2个String
c.setNames("henan"); // 传入1个String
c.setNames(); // 传入0个String
}
}
class City{
private String[] names;
public void setNames(String... names) { //setNames()定义了一个可变参数
this.names = names;
}
}
把可变参数改写为String[]
类型需要自己先构造String[]
,比较麻烦,但是可以传入null
。例如:
class Group {
private String[] names;
public void setNames(String[] names) {
this.names = names;
}
}
Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]
g.setNames(null);
可变参数可以保证无法传入
null
,因为传入0个参数时,接收到的实际值是一个空数组而不是null
。
区分:
例1:
// 基本类型参数绑定
public class Main {
public static void main(String[] args) {
Person p = new Person();
int n = 15; // n的值为15
p.setAge(n); // 传入n的值
System.out.println(p.getAge()); // 15
n = 20; // n的值改为20
System.out.println(p.getAge()); // 15还是20?
}
}
class Person {
private int age;
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
15
15
从结果可知,修改外部的局部变量n
,不影响实例p
的age
字段,原因是setAge()
方法获得的参数,复制了n
的值,因此,p.age
和局部变量n
互不影响。
结论:基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
例2:
// 引用类型参数绑定
public class Main {
public static void main(String[] args) {
Person p = new Person();
String[] fullname = new String[] { "Homer", "Simpson" };
p.setName(fullname); // 传入fullname数组
System.out.println(p.getName()); // "Homer Simpson"
fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart"
System.out.println(p.getName()); // "Homer Simpson"还是"Bart Simpson"?
}
}
class Person {
private String[] name;
public String getName() {
return this.name[0] + " " + this.name[1];
}
public void setName(String[] name) {
this.name = name;
}
}
Homer Simpson
Bart Simpson
注意到setName()
的参数现在是一个数组。把fullname
数组传进去,然后,修改fullname
数组的内容,结果发现,实例p
的字段p.name也被修改了!
结论:引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方。
例3:
public class Main {
public static void main(String[] args) {
Person p = new Person();
String bob = "Bob";
p.setName(bob); // 传入bob变量
System.out.println(p.getName()); // "Bob"
bob = "Alice"; // bob改名为Alice
System.out.println(p.getName()); // "Bob"还是"Alice"?
}
}
class Person {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
Bob
Bob
个人理解:bob = "Alice"时会在内存中新开辟一个空间,bob的指向变成Alice。但是name属性还是指向原来的内存空间,还是Bob。
举例:A和B都有一个苹果,A吃了半个苹果,那么B的苹果是一个还是半个?
构造方法
构造方法可以在创建对象实例时就把内部字段全部初始化为合适的值。
public class Main {
public static void main(String[] args) {
Person p = new Person("Xiao Ming", 15);
System.out.println(p.getName());
System.out.println(p.getAge());
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
Xiao Ming
15
构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void
),调用构造方法,必须用new
操作符。
任何class
都有构造方法,如果一个类没有定义构造方法,编译器会自动生成一个默认构造方法,它没有参数,也没有执行语句:
class Person {
public Person() {
}
}
如果自己定义一个构造方法,那么编译器就不再自动创建默认构造方法。
注意:
有构造方法(带参):
Person p = new Person("Xiao Ming", 15);
默认情况下(不带参):
Person p = new Person();
如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来。
没有在构造方法中初始化字段时:
引用类型的字段默认是null
,
数值类型的字段用默认值,
int
类型默认值是0
,
布尔类型默认值是false
若既对字段进行初始化,又在构造方法中对字段进行初始化:
class Person {
private String name = "Unamed";
private int age = 10;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
当创建new Person("Xiao Ming", 12)
:
在Java中,创建对象实例的时候,按照如下顺序进行初始化:
1.先初始化字段,例如,int age = 10;表示字段初始化为10。
2.执行构造方法的代码进行初始化。
因此,构造方法的代码由于后运行,所以,new Person("Xiao Ming", 12)
的字段值最终由构造方法的代码确定。
多构造方法
在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this.name = name;
this.age = 12;
}
public Person() {
}
}
一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)
:
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
}
public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String)
}
}
方法重载
在一个类中可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。
方法名相同,但各自的参数不同,称为方法重载。
注意:方法重载的返回值类型通常都是相同的。
例子:
class Hello {
public void hello() {
System.out.println("Hello, world!");
}
public void hello(String name) {
System.out.println("Hello, " + name + "!");
}
public void hello(String name, int age) {
if (age < 18) {
System.out.println("Hi, " + name + "!");
} else {
System.out.println("Hello, " + name + "!");
}
}
}
继承
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。当我们让Student
从Person继承
时,Student
就获得了Person
的所有功能,我们只需要为Student
编写新增的功能。
Java使用extends
关键字来实现继承,通过继承,Student
只需要编写额外的功能,不再需要重复代码。
注意:
子类自动获得了父类的所有字段,严禁定义与父类重名的字段!
在OOP的术语中,我们把Person
称为超类(super class),父类(parent class),基类(base class),把Student称为子类(subclass),扩展类(extended class)。
protected
继承的子类无法访问父类的private
字段或者private
方法。为了让子类可以访问父类的字段,需要把private
改为protected
。
因此,protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected
字段和方法可以被其子类,以及子类的子类所访问。
super
super
关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName
。
class Student extends Person {
public String hello() {
return "Hello, " + super.name;
}
}
实际上,这里使用super.name
,或者this.name
,或者name
,效果都是一样的。编译器会自动定位到父类的name
字段。
但是,在某些时候,就必须使用super
。
public class Main {
public static void main(String[] args) {
Student s = new Student("Xiao Ming", 12, 89);
}
}
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
this.score = score;
}
} //编译错误,在Student的构造方法中,无法调用Person的构造方法。
因为在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会自动加一句super();。
但是Person
类并没有无参数的构造方法,因此,编译失败,修改为:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}
结论:
如果父类没有默认的构造方法,子类就必须显式调用super()
并给出参数以便让编译器定位到父类的一个合适的构造方法。
即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
应用:
public class Main {
public static void main(String[] args) {
Student ming = new Student();
ming.setName("Xiao Ming");
ming.setScore(100);
System.out.println(ming.getName());
System.out.println(ming.getScore());
}
}
class Person {
protected String name;
protected int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Student extends Person{
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
}
Xiao Ming
100
阻止继承
正常情况下,只要某个class没有final
修饰符,那么任何类都可以从该class
继承。
从Java 15开始,允许使用sealed
修饰class,并通过permits
明确写出能够从该class继承的子类名称。
例如:
public sealed class Shape permits Rect, Circle, Triangle {
...
} //Shape类就是一个sealed类,只允许指定的3个类继承它。
区分:
public final class Rect extends Shape {...} //可以
public final class Ellipse extends Shape {...} //不可以
// Compile error: class is not allowed to extend sealed class: Shape
sealed
类主要用于一些框架,防止继承被滥用。
sealed
类在Java 15中目前是预览状态,要启用它,必须使用参数--enable-preview
和--source 15
。
向上转型
把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
Person p = new Student(); // 可以执行
这是因为Student
继承自Person
,因此,它拥有Person
的全部功能。Person
类型的变量,如果指向Student
类型的实例,对它进行操作,是没有问题的!
Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok
继承树是Student > Person > Object
,所以,可以把Student
类型转型为Person
,或者更高层次的Object
。
向下转型
和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型。
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
理解
子类转父类安全,父类转子类不安全
Person
类型p1
实际指向Student
实例,Person
类型变量p2
实际指向Person
实例。
在向下转型的时候,把p1
转型为Student
会成功,因为p1
确实指向Student
实例,把p2
转型为Student
会失败,因为p2
的实际类型是Person
,不能把父类变为子类。
因为子类功能比父类多,多的功能无法凭空变出来。
为了避免向下转型出错,Java提供了instanceof
操作符,可以先判断一个实例究竟是不是某种类型:
package exercise;
public class Main {
public static void main(String arg[]){
Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false
Person q = new Student();
System.out.println(q instanceof Person); // true
System.out.println(q instanceof Student); // true
Student n = null;
System.out.println(n instanceof Student); // false
}
}