# Java教程 - 7 面向对象
# 7.7 Object类
如果一个类没有继承其他的类,那么这个类就是继承自 Object 类,所以最终所有的类都会继承自 Object。
举个栗子:
class Bird { }
class Pigeon extends Bird { }
public class ExtendTest {
public static void main(String[] args) {
Pigeon pigeon = new Pigeon();
System.out.println(pigeon.getClass().getSuperclass()); // class com.doubibiji.Bird
Bird bird = new Bird();
System.out.println(bird.getClass().getSuperclass()); // class java.lang.Object
}
}
2
3
4
5
6
7
8
9
10
11
12
13
可以看到 Bird 类的父类是 Object。所以最终所有的类都会继承 Object,并拥有 Object 中的方法。
Object 中我们最常用的就是: toString()
和 equals()
方法。
# 1 toString()
toString()
方法用于返回表示对象的字符串表示形式。当我们打印对象的时候,就会将对象转换为字符串进行输出,就会调用 toString()
方法。
因为所有的类都是继承自 Object 类的,所以我们在类中不写 toString()
方法,默认是调用了 Object 类的 toString()
方法。
举个栗子:
class Student {
public String sid;
public String name;
public int age;
}
public class ObjectTest {
public static void main(String[] args) {
Student stu = new Student();
System.out.println(stu);
}
}
2
3
4
5
6
7
8
9
10
11
12
打印对象会输出:对象的全类名(包名+类名)和对象的哈希码的无符号十六进制表示。例如:com.doubibiji.Student@d041cf
。
因此,如果想修改对象打印时候的显示信息,可以在自定义类中重写 toString()
方法,以便更好地控制对象的字符串表示形式。
举个栗子:
class Student {
public String sid;
public String name;
public int age;
/**
* 重写toString()方法
*/
@Override
public String toString() {
return "Student{" +
"sid=" + sid +
", name=" + name +
", age=" + age +
"}";
}
}
public class ObjectTest {
public static void main(String[] args) {
Student stu = new Student();
stu.sid = "001";
stu.name = "逗比";
stu.age = 5;
System.out.println(stu); // 输出:Student{sid=001, name=逗比, age=5}
}
}
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
打印对象,会调用 toString()
方法,打印 toString() 方法返回的内容。
# 2 equals()
在比较基本数据类型变量的时候,我们使用的是 ==
进行比较,比较的是两个变量的值。
而对于引用类型的变量,使用 ==
进行比较,比较的也是两个变量的值,但是引用类型的变量存储的值是对象在堆中的地址,所以最终比较的是两个对象的地址,但是对于属性完全相同的两个对象,使用 ==
比较也是不同的。
举个栗子:
class Student {
public String name;
public Student(String name) {
this.name = name;
}
}
public class ObjectTest {
public static void main(String[] args) {
String str1 = new String("doubibiji");
String str2 = new String("doubibiji");
System.out.println(str1.equals(str2)); // true
Student stu1 = new Student("doubibiji");
Student stu2 = new Student("doubibiji");
System.out.println(stu1.equals(stu2)); // false
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
为什么两个相同字符串的 String 类的对象,使用 equals 比较是相等的,但是对于相同 name 属性的 Student 类的对象又是不等的呢?
因为 String 和 Student 类都继承自 Object 类,Object 类中的 equals 方法中,使用的是 ==
比较两个对象,所以比较的是两个对象的地址。但是 String 类重写了 equals 方法,修改为比较内容。所以如果我们想在比较 Student 两个对象的时候,比较 name 相等就算相等,那么也需要修改 equals 方法。
class Student {
public String name;
public Student(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
// 如果地址相等,说明是同一个对象
if (this == obj) {
return true;
}
// 如果obj都不是Student类型,直接return
if (!(obj instanceof Student)) {
return false;
}
Student other = (Student) obj;
if (null != name) {
return name.equals(other.name);
}
else {
return false;
}
}
}
/**
* 测试类
*/
public class ObjectTest {
public static void main(String[] args) {
Student stu1 = new Student("doubibiji");
Student stu2 = new Student("doubibiji");
Student stu3 = new Student("doubi");
System.out.println(stu1.equals(stu2)); // true
System.out.println(stu1.equals(stu3)); // false
}
}
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
在上面的 Student 类中重写了 equals 方法,比较两个对象的 name 属性,如果相等,两个对象使用 equals 比较就算相等。
在 IDEA 中,在类中右键选择 Generate... -> equals() and hashcode()
就可以自动生成 equals() 和 hashcode() 方法,正常情况下重写 equals()
方法一定要重写 hashcode()
,这里没涉及到其他功能,只重写了 equals()
方法。讲集合的时候再讲为什么还需要重写 hashcode()
方法。
# 7.8 包装类
Java 是面向对象语言,在 Java 中一切皆对象。
但是基本数据类型独立之外,为了使这些基本数据类型也具有对象的特性,Java引入了对应的包装类,使得基本数据类型也能像对象一样进行操作。
Java 中针对 8 中基本数据类型提供了包装类,分别是:
- Integer:int
- Byte:byte
- Short:short
- Long:long
- Float:float
- Double:double
- Character:char
- Boolean:boolean
所以这里涉及到三种数据的转换:基本数据类型,包装类、字符串之间的转换。
下面的转换以 Int 和 Integer 为例,其他的都是相似的。
# 1 基本数据类型-包装类
本来在进行数学运算的时候,只能使用基本数据类型,在将数据放入到集合(后面的章节讲)中的时候,只能使用包装类。所以要进行相互转换。
但是在 JDK1.5 后提供了自动装箱和拆箱的功能,也就是基本数据类型和包装类可以自动进行转换。
自动装箱:基本数据类型自动转换为包装类;
自动拆箱:包装类自动转换为基本数据类型。
public class WrapperTest {
public static void main(String[] args) {
// 基本数据类型转包装类
// 方式1:自动装箱,推荐用法
Integer a = 123;
// 方式2
Integer b = Integer.valueOf(123);
// 方式3:过期了,不推荐使用
Integer c = new Integer(123);
// 包装类转基本数据类型
// 方式1:自动拆箱,推荐
int x = a;
// 方式2
int y = a.intValue();
int d = a + b; // 自动拆箱,包装类可以参与数学运算
printInteger(d); // 传递参数自动装箱
printInt(a); // 传递参数自动拆箱
}
public static void printInt(int i) {
System.out.println(i);
}
public static void printInteger(Integer i) {
System.out.println(i);
}
}
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
因为可以自动装箱和拆箱,所以在调用方法的时候,基本数据类型和包装类的参数可以自动转换,同时包装类也可以进行数学运算了。
# 2 包装类-字符串
我们经常需要将字符串类型的数字转换为整数,例如从键盘输入的内容,所以就需要字符串和包装类的转换。
public static void main(String[] args) {
Integer a = 123;
// 包装类转字符串
// 方式1
String s1 = a.toString();
// 方式2
String s2 = "" + a;
// 字符串转包装类
Integer b = Integer.valueOf("123");
}
2
3
4
5
6
7
8
9
10
11
# 3 基本数据类型-字符串
和上面包装类转字符串其实是可以合并到一起的,因为包装类和字符串可以相互转换。
public static void main(String[] args) {
// 基本数据类型转字符串
int a = 123;
// 方式1
String s1 = String.valueOf(a);
// 方式2
String s2 = Integer.toString(a);
// 方式3
String s3 = "" + a;
// 字符串转基本数据类型
String str = "123";
int i = Integer.parseInt(str);
int j = Integer.valueOf(str); // 这里转的是包装类,但是可以自动拆箱
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 4 缓存Integer
看一下下面的代码:
public class WrapperTest {
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false
}
}
2
3
4
5
6
7
8
9
10
11
懵逼不懵逼?为什么呢?
在 Java 中,对于Integer
类型的值,当值在-128到127之间(包括-128和127)时,Java会缓存这些对象,以避免频繁地创建和销毁小整数对象。所以在-128到127之间创建 Integer 对象,会去缓存中找对应的 Integer 对象返回,所以多次创建返回的都是相同的对象。
所以比较 Integer 对象,使用 equals()
比较。
# 7.9 final 关键字
final
表示最终的意思,final
关键字可以用来修饰类、方法、变量。
分别有什么作用呢?
# 1 修饰类
final
关键字修饰类,那么类不能被其他类继承。
final public class Student { // Studnet类无法被其他类继承
}
2
3
在 JDK 中,String类、System 等类都是 final 的,是无法被继承的。
# 2 修饰方法
final 关键字修饰方法,方法是无法被重写的。
class Bird {
// tweet无法被子类重写
final public void tweet() {
System.out.println("我会叫");
}
}
2
3
4
5
6
# 3 修饰变量
final 关键字修饰变量,包括全局变量,也就是类中的属性,也包括局部变量,也就是方法的形参和方法中定义的变量。
修饰类中的属性
类中的属性使用 final 修饰后,一旦初始化,就无法修改属性的值了。
举个栗子:
class Student {
private final String name;
public Student(String name) {
this.name = name;
//this.name = "abc"; // 报错,上面已经初始化,无法再更改值。
}
}
2
3
4
5
6
7
8
9
在上面的代码中,Student 中的成员变量 name 使用 final 修饰,那么属性一旦被初始化,就无法再更改了。
对于成员变量而言,可以使用显式初始化、代码块中初始化、构造方法中初始化都可以。
对于静态变量而言,可以使用显式初始化、静态代码块中初始化。
修饰局部变量
可以修饰方法中的变量和方法的参数。
class Course {
public String name;
}
class Student {
/**
* 方法一
*/
public final void study(final int duration) {
// duration = 123; // 错误,无法修改
final int a = 123;
// a = 234; // 错误,初始化后无法修改。
}
/**
* 方法二
*/
public final void test(final Course course) {
// course = new Course(); // 错误,无法修改变量值,所以无法修改变量的指向
course.name = "Chinese"; // 可以,没有修改变量course的指向,只是修改变量的属性
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这里需要注意,当修饰的变量是引用类型的时候,我们虽然无法修改变量的指向的堆中的对象,但是可以修改对象的属性。
# 4 定义全局常量
在实际的开发中,我们一般使用 static 和 final 来共同修饰全局变量,此时的变量的值是无法修改的,也就成了全局的常量。
举个例子:
class AppCostants {
// 定义用户名最大长度
public static final int USERNAME_MAX_LENGTH = 16;
// 定义用户名最小长度
public static final int USERNAME_MIN_LENGTH = 8;
}
2
3
4
5
6
定义常量的时候,常量的名称一般统一为大些,单词之间使用下划线分隔。
# 7.10 匿名子类和匿名实现类
抽象类是无法创建实例的,所以我们可以继承抽象来,通过子类来创建对象。
但是有时候创建的子类只想使用一次,不想单独定一个类,那么也可以使用抽象类来创建匿名子类。
举个栗子,首先有一个抽象类:
abstract class Bird {
abstract public void fly();
abstract public void tweet();
}
2
3
4
5
下面使用匿名子类,重写 Bird 类中的抽象方法,实现自己的逻辑:
public class TestClass {
public static void main(String[] args) {
Bird bird = new Bird() {
@Override
public void fly() {
System.out.println("我会飞");
}
@Override
public void tweet() {
System.out.println("我会叫");
}
};
bird.fly();
bird.tweet();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在上面的代码中,并不是创建了 Bird 类的实例,而是创建的 Bird 类的子类的实例。然后重写了 Bird 的抽象方法。
同样的方式,还可以创建接口匿名实现类:
interface Bird {
void fly();
void tweet();
}
public class TestClass {
public static void main(String[] args) {
Bird bird = new Bird() {
@Override
public void fly() {
System.out.println("我会飞");
}
@Override
public void tweet() {
System.out.println("我会叫");
}
};
bird.fly();
bird.tweet();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 7.11 内部类
内部类在实际的开发中用的不多,只是在 Android 开发中用的多点。
Java 允许在一个类中再定义类,这个类内部的类就是内部类。一般情况下,当一个类只为另一个类提供服务的函数,可以将这个类定义为内部类。
在 Java 中内部类有:
- 成员内部类(static 和 非 static 的内部类)
- 局部内部类(方法内、代码块内、构造方法内)
因为内部类属于外部类的成员,所以和类中的属性、方法等有一些共同之处,例如可以使用 static 来修饰,可以使用权限修饰符(public、protected等)修饰。
# 1 定义内部类
举个栗子:
class Student {
String name = "逗比";
static int age = 12;
public static void test1() {
System.out.println("我要哔哔两句");
}
public void test2() {
System.out.println("我爱学习");
}
// 静态成员内部类
static class Brain {
public void test1() {
// 静态内部类无法访问外部类非静态属性和方法,因为静态内部类属于外部类的静态成员,在类加载的时候加载,此时还没有类的实例
// System.out.println(name); // 报错
// test2(); // 报错
// 可以调用外部类的静态属性和方法
System.out.println(age);
// 内部类和外部类方法同名,使用类名来访问
Student.test1();
}
}
// 非静态成员内部类
class Heart {
private String name;
public void test2(String name) {
System.out.println(name); // 访问方法形参
System.out.println(this.name); // 访问内部属性
System.out.println(Student.this.name); // 访问外部属性
// 如果属性不重名,直接使用name属性名就可以访问
// 内部类和外部类方法同名,使用Student.this.beat() 调用外部类
Student.this.test2(); // 合法,调用外部类成员方法
// 可以调用外部类的静态属性和方法
System.out.println(age);
test1();
}
}
{
// 代码块中定义的局部内部类
class ClassA {
}
}
public Student() {
// 构造方法中定义的局部内部类
class ClassB {
}
}
/**
* 在局部内部类中的方法中,如果想调用该内部类所在的方法中的局部变量,那么外部类方法中的变量需要声明为final的
*/
public void test() {
int flag = 10;
// 方法中定义的局部内部类
class ClassC {
public void test2() {
System.out.println(flag); // 可以访问,jdk8不用显式声明为final,之前的需要显式声明
// flag = 20; // 非法,final的,无法修改
System.out.println(name); // 合法,调用外部类成员属性
// 内部类和外部类方法同名,使用Student.this.beat() 调用外部类
Student.this.test2(); // 合法,调用外部类成员方法
// 可以调用外部类的静态属性和方法
System.out.println(age);
test1();
}
}
}
}
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
在上面的代码中,演示了各种内部类的定义,以及在内部类中访问外部类成员的方式。
在实际的开发中,局部内部类一般是在方法中返回一个内部类的对象,例如下面演示了实现了 Comparable
接口的内部类,并创建对象返回。
// 定义方法
public Comparable test() {
// 定义内部类
class MyComparable implements Comparable {
@Override
public int compareTo(Object o) {
return 0;
}
}
// 返回内部类对象
return new MyComparable();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这种方式和使用接口的匿名实现类基本是一样的:
public Comparable test() {
return new Comparable() {
@Override
public int compareTo(Object o) {
return 0;
}
};
}
2
3
4
5
6
7
8
# 2 实例化内部类
如果是在外部类中实例化内部类,直接 new
就可以了。
如果是在外部类的外面实例化内部类,就需要使用如下方式:
public class ObjectTest {
public static void main(String[] args) {
// 创建静态内部类实例
Student.Brain brain = new Student.Brain();
brain.test1();
// 创建非静态内部类实例,首先得有外部类的对象
Student stu = new Student();
Student.Heart heart = stu.new Heart();
heart.test2("zhangsan");
}
}
2
3
4
5
6
7
8
9
10
11
12