更好的阅读体验:点这里 ( www.doubibiji.com
)
更好的阅读体验:点这里 ( www.doubibiji.com
)
更好的阅读体验:点这里 ( www.doubibiji.com
)
7 面向对象
面向对象,首先涉及到的两个概念就是:类 和 对象。
什么是类?
类就是对现实事物的抽象设计。
例如设计学生的类,可能包括属性:学号,姓名、年龄、性别等。
设计狗的类,可能包括属性:名字、年龄、品种。
类表示的是对一个事物的抽象,不是具体的某个事物。
什么是对象?
对象就是一个具体的实例,这个实例是从类派生出来的。
我们将类的属性赋值,就产生了一个个实例,也就是对象。
例如通过学生的类,我们赋值:学号=001,姓名=张三,年龄=16,性别=男,班级=三年二班,产生了就是一个名字叫张三的具体的学生,这样我们通过给类可以派生出一个个不同属性值的对象,李四、王五、赵六…。
所以是先设计类,然后根据类来生成一个个对象。
7.1 类和对象
1 类的定义
类中可以定义属性和行为,属性也就是成员变量,表示这个类有哪些数据信息,行为也叫成员方法,表示这个类能干什么。
例如,对于学生类而言,学号、姓名、年级就是属性,学习这个行为,可以定义为方法。
那么我们可以定义以下学生类:
/**
*定义类使用class关键字,后面跟类名
*/
class Student {
public String sid;
public String name;
public int age;
public void study() {
System.out.println("我是" + name + ", 我在学习");
}
}
上面的类定义了三个成员变量(sid、name、age),public
表示访问修饰符,用来限制属性的访问权限,后面再讲,现在使用 public
。后面是变量类型和变量的名称。
还定义了一个成员方法 study()
,定义成员方法和之前定义静态方法是一样的,只是没有 static
关键字。
2 类的使用
上面我们定义好类了,现在可以使用类来创建对象了。
/**
* 定义学生类
*/
class Student {
public String sid;
public String name;
public int age;
public void study() {
System.out.println("我是" + name + ", 我在学习");
}
}
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
Student stu = new Student(); // 创建对象
stu.sid = "001"; // 为对象的sid属性赋值
stu.name = "张三"; // 为对象的name属性赋值,现在学生是张三了
stu.age = 18;
System.out.println(stu.name); // 打印名称
stu.study(); // 使用对象调用方法
}
}
因为 Java 中所有的代码都要放到类中,这里将测试的 main 方法放到了另一个类中,当然 main 方法放到 Student 中也是可以的,这里为了结构清晰。
首先通过 new Student()
可以创建一个 Student 对象,赋值给 Student
类型的变量 stu
。
创建对象后,我们可以通过 对象.属性
来访问变量,也可以通过 对象.属性=值
来给属性赋值。
使用 对象.方法()
可以调用方法。
执行结果:
张三
我是张三, 我在学习
面向对象编程就是先设计类,然后通过类创建对象,由对象做具体的工作。
3 默认构造方法
在上面创建对象后,使用 对象.属性=值
来给属性赋值,有点麻烦,我们可以在创建对象的时候,直接传递参数,给属性赋值。
这里就需要用到 构造方法。构造方法会在创建对象的时候自动执行,通过传递的属性值,给属性进行初始化。
举个栗子:
构造方法的名称和类的名称是一致的,没有返回值类型。
/**
* 定义类
*/
class Student {
public String sid;
public String name;
public int age;
/**
* 构造方法
*/
public Student(String sid, String name, int age) {
this.sid = sid; // this.sid 访问的是属性,sid 是方法的形参
this.name = name;
this.age = age;
}
public void study() {
System.out.println("我是" + name + ", 我在学习");
}
}
/**
* 测试类
*/
public class Test{
public static void main(String[] args) {
// 创建张三
Student stu1 = new Student("001", "张三", 18);
stu1.study();
// 创建李四
Student stu2 = new Student("002", "李四", 19);
stu2.study();
}
}
在上面的代码中,定义了构造方法,接收三个参数 (String sid, String name, int age)
,在构造方法中,分别将三个参数赋值给类中的成员变量。
因为构造方法的参数和类中的成员变量名称相同了(也可以不同),所以如果在构造方法中直接使用变量名,访问的将是构造方法的参数,访问不到成员变量,如果要访问成员,需要使用 this
关键字。
当然在 study()
方法中,没有参数和成员变量 name
同名,所以 name
访问到的就是成员变量,可以不用 this
(当然使用也没毛病)。
在创建对象的时候,就可以传递参数为对象的属性赋值: new Student("001", "张三", 18)
。每次 new
就会创建新的对象,每个对象是独立的,所以上面张三和李四对象是独立的两个对象,修改其中一个对象的值,不会影响另一个对象。
执行结果:
我是张三, 我在学习
我是李四, 我在学习
4 this的作用
在上面的代码中用到了 this
,因为上面我们写构造方法的时候,形参和类的属性名相同(当然也可以不同),导致在构造方法中,使用 sid/name/age
无法访问到类的属性,使用this,就表示访问的是属性 。
其实 this
表示的是调用当前方法的对象。如何理解?
举个栗子:
下面的代码,我们定义了学生类,创建了两个学生的对象。
/**
* 定义类
*/
class Student {
public String sid;
public String name;
public int age;
/**
* 构造方法
*/
public Student(String sid, String name, int age) {
this.sid = sid; // this.sid 访问的是属性,sid 是方法的形参
this.name = name;
this.age = age;
}
public void study() {
System.out.println("我是" + this.name + "我" + age +"岁了" + ", 我在学习");
}
}
/**
* 测试类
*/
public class Test{
public static void main(String[] args) {
// 创建张三
Student stu1 = new Student("001", "张三", 18);
stu1.study();
// 创建李四
Student stu2 = new Student("002", "李四", 19);
stu2.study();
}
}
执行结果:
我是张三, 我18岁了, 我在学习
我是李四, 我19岁了, 我在学习
当我们使用 stu1
调用 study()
方法的时候,this
就是指 张三
(stu1)这个对象,那么 this.name
的值就是张三;当我们使用 stu2
调用 study()
方法的时候,this
就是指 李四
(stu2) 这个对象,那么 this.name
的值就是李四。this
就是调用当前方法的对象。
5 对象内存解析
先查看下面的代码:
Student stu1 = new Student(); // 创建张三对象
stu1.sid = "001";
stu1.name = "张三";
stu1.age = 18;
Student stu2 = new Student(); // 创建李四对象
stu2.sid = "002";
stu2.name = "李四";
stu2.age = 19;
Student stu3 = stu1;
stu3.name = "王五";
System.out.println(stu1.name); // 王五
System.out.println(stu2.name); // 李四
System.out.println(stu3.name); // 王五
在上面的代码中,首先创建了两个对象赋值给了 stu1 和 stu2,并对属性进行赋值,然后定义了一个 stu3 的变量,将 stu1 赋值给了 stu3,我们前面说过 stu1 和 stu2 是独立的两个对象,那么 stu1 和 stu3 是独立的两个对象吗?两个是同一个对象。下面画一下内存示意图:
创建完 stu1 并赋值后,内存结构如下(其实字符串是不保存在堆或栈中的,这里简化了):
创建完 stu2 并赋值后,内存结构如下:
Student stu3 = stu1
执行完,内存结构如下:
stu3.name = "王五";
执行完成,内存结构如下:
所以只在 new 的时候才会创建新的对象,stu3 = stu1
只是将 stu1 执行的对象的引用赋值给了 stu3 。
6 静态变量
上面我们定义的属性和方法,是实例变量和实例方法,也叫成员变量和成员方法。
实例变量对于每个实例而言,是独立的数据,每个对象之间相互不会影响。创建一个对象,就会开辟独立的内存空间保存对象的实例变量数据。但是无论创建多少对象,实例方法只有一份,所有对象共享,通过 this
,来确定是哪个对象调用了实例方法。
在类中还可以定义各个对象共享的数据,也就是静态变量。
打个比方,我们定义了一个 Student 类,然后通过 Student 类来创建对象,我们想知道一共创建了多少个 Student 对象,应该如何操作呢?
我们可以通过定义 静态变量 来实现,静态变量和成员变量的区别就是:前面通过 static
关键字来修饰。
/**
* 定义类
*/
class Student {
public static int stuCount = 0; // 定义一个静态变量,用于记录创建的对象个数
public String sid;
public String name;
public int age;
/**
* 构造方法
*/
public Student(String sid, String name, int age) {
stuCount++; // 创建对象会调用构造方法,调用一次就+1
this.sid = sid; // this.sid 访问的是属性,sid 是方法的形参
this.name = name;
this.age = age;
}
}
/**
* 测试类
*/
public class Test{
public static void main(String[] args) {
Student stu1 = new Student("001", "张三", 18);
Student stu2 = new Student("002", "李四", 19);
Student stu3 = new Student("003", "王五", 20);
// 通过类名访问
System.out.println(Student.stuCount); // 输出: 3
// 通过对象也可以访问
System.out.println(stu1.stuCount); // 输出: 3
System.out.println(stu2.stuCount); // 输出: 3
System.out.println(stu3.stuCount); // 输出: 3
}
}
在上面的代码中,我们定义了一个类,然后在类中定义了一个 stuCount
静态变量。当创建对象的时候,会调用构造方法,我们在构造方法中将 stuCount++
,这样就可以记录调用构造方法的次数。
静态变量用来定义那些所有对象共享的数据。
**静态变量是属于类的,而不是属于类的实例。在类的方法中,如果没有局部变量和静态变量重名,那么可以直接使用静态变量,就像上面在构造方法中一样,如果有局部变量和静态变量重名,可以使用 类名.静态变量
来访问。 **
在类外,像上面在 Test 类中访问 Student 类中的静态变量,那么可以通过 类名.静态变量
来赋值和访问,也可以通过 对象.静态变量
来访问,但是推荐使用 类名.静态变量
。
7 静态方法
除了静态变量,还有静态方法。静态变量也是属于类的,而不是属于类的实例。
静态方法通过 static
关键字来修饰的方法,之前在学习方法的时候,我们定义的全是静态方法。
/**
* 定义类
*/
class Student {
public static int stuCount = 0; // 定义一个静态变量,用于记录创建的对象个数
public String sid;
public String name;
public int age;
/**
* 构造方法
*/
public Student(String sid, String name, int age) {
stuCount++; // 创建对象会调用构造方法,调用一次就+1
this.sid = sid; // this.sid 访问的是属性,sid 是方法的形参
this.name = name;
this.age = age;
}
public void study() {
System.out.println("我正在学习");
}
/**
* 定义静态方法
*/
public static void getStuCount() {
System.out.println("一共创建了" + stuCount + "个学生");
// System.out.println(name); // 静态方法中无法访问成员变量
// study(); // 静态方法中无法调用成员方法
}
}
/**
* 测试类
*/
public class Test{
public static void main(String[] args) {
// 创建张三
new Student("001", "张三", 18);
// 创建李四
new Student("002", "李四", 19);
// 创建王五
new Student("003", "王五", 20);
Student.getStuCount(); // 输出: 一共创建了3个学生
}
}
在静态方法不能访问成员变量和成员方法,因为静态方法是通过 类.静态方法()
调用的,不是通过对象实例来调用的,所以如果在静态方法中调用的成员变量没法确定是哪个实例的变量。但是在成员方法中是可以访问静态变量和静态方法的。
静态方法一般用来定义一些工具类,例如定义一个字符串的工具类:
public class StringUtils {
/**
* 判断字符串是否为空
*/
public static boolean isEmpty(String arg) {
// trim()方法是去掉字符串的前后空格
return (null == arg || arg.trim().length() < 1);
}
/**
* 判断字符串是否不为空
*/
public static boolean isNotEmpty(String arg) {
return !isEmpty(arg);
}
// ...定义其他的工具方法
}
定义完工具类,我就可以在其他的类中,通过 StringUtils.isEmpty("字符串")
来调用这个工具类中的方法了。
8 局部变量和全局变量
变量的作用域也就是在哪里可以访问到这个变量。按照作用域的不同,变量可分为 局部变量 和 全局变量。
什么是局部变量?
局部变量就是在方法中(包括方法的参数)或代码块中(例如for循环中定义的变量)定义的变量,这些变量只能在该方法中进行访问,其他方法中无法访问。
什么是全局变量?
全局变量就是类的属性,这些变量可以在多个方法中进行访问。
局部变量和全局变量除了作用域不同,还有一些地方也不同:
- 权限修饰符:全局变量是类的属性,可以添加权限修饰符,我们目前只使用了
public
,局部变量没有权限修饰符,关于权限修饰符后面再讲解; - 初始化:全局变量有初始化值,而局部变量在使用前,需要自己进行初始化,否则无法使用;
- 内存中的位置:全局变量是保存在堆中的,而局部变量(非static,static是保存在虚拟机的方法区中的)是保存在栈中的。
关于全局变量的初始化值,在这里举个栗子:
/**
* 定义类
*/
class DataClass {
public byte byteValue;
public short shortValue;
public int intValue;
public long longValue;
public float floatValue;
public double doubleValue;
public boolean booleanValue;
public char charValue;
public String stringValue;
}
/**
* 测试类
*/
public class Test{
public static void main(String[] args) {
DataClass data = new DataClass();
System.out.println(data.byteValue); // 0
System.out.println(data.shortValue); // 0
System.out.println(data.intValue); // 0
System.out.println(data.longValue); // 0
System.out.println(data.floatValue); // 0.0
System.out.println(data.doubleValue); // 0.0
System.out.println(data.booleanValue); // false
char c = 0;
System.out.println(data.charValue == c);// true
System.out.println(data.stringValue); // null
}
}
在上面的代码中,我们并没有对类的属性进行初始化,但是类中的属性会默认有初始化值。
byte、short、int、long、char
默认初始化值为 0
,float、double
默认初始化值为 0.0
,boolean
默认初始化值为 false
,引用类型
的默认初始化值为 null
。
9 构造方法的重载
如果一个类中,没有显式的构造方法,那么会有一个隐式的无参构造方法。
class Student {
public String sid;
public String name;
public int age;
}
public class ObjectTest {
public static void main(String[] args) {
// 这里的Student()调用的就是默认隐式的构造方法
Student stu = new Student();
}
}
此时创建 Student 类对象,就是调用的隐式无参构造方法。当然,我们也可以手动写一个无参构造方法。
class Student {
public String sid;
public String name;
public int age;
public Student() {
System.out.println("无参构造方法");
}
}
public class ObjectTest {
public static void main(String[] args) {
Student stu = new Student(); // 打印:无参构造方法
}
}
当我们手动写了显式的无参构造方法,那么就会调用这个显式的无参构造方法了。
如果我们显式的写了有参的构造方法,那么隐式的无参构造方法就没有了,所以在创建对象的时候,就必须传递参数。
举个栗子:
class Student {
public String sid;
public String name;
public int age;
public Student(String sid, String name, int age) {
this.sid = sid;
this.name = name;
this.age = age;
}
}
public class ObjectTest {
public static void main(String[] args) {
// Student stu = new Student(); // 报错,没有无参构造方法了
Student stu = new Student("001", "ZhangSan", 18);
}
}
当然构造方法也是可以重载的,我们可以定义多个构造方法。
class Student {
public String sid;
public String name;
public int age;
public Student() {
System.out.println("无参构造方法");
}
public Student(String sid, String name) {
System.out.println("有参构造方法: sid, name");
this.sid = sid;
this.name = name;
}
public Student(String sid, String name, int age) {
System.out.println("有参构造方法: sid, name, age");
this.sid = sid;
this.name = name;
this.age = age;
}
}
public class ObjectTest {
public static void main(String[] args) {
Student stu1 = new Student();
Student stu2 = new Student("001", "ZhangSan");
Student stu3 = new Student("001", "ZhangSan", 18);
}
}
执行结果:
无参构造方法
有参构造方法: sid, name
有参构造方法: sid, name, age
那么如何在一个构造方法中调用另一个构造方法呢?
使用 this([参数])
来调用,举个栗子:
class Student {
public String sid;
public String name;
public int age;
public Student() {
System.out.println("无参构造方法");
}
public Student(String sid, String name) {
this(); // 调用无参构造方法
System.out.println("有参构造方法: sid, name");
this.sid = sid;
this.name = name;
}
public Student(String sid, String name, int age) {
this(sid, name); // 调用其他的构造方法
System.out.println("有参构造方法: sid, name, age");
this.sid = sid;
this.name = name;
this.age = age;
}
}
public class ObjectTest {
public static void main(String[] args) {
Student stu1 = new Student();
Student stu2 = new Student("001", "ZhangSan");
Student stu3 = new Student("001", "ZhangSan", 18);
}
}
需要注意,使用 this([参数]) 调用其他的构造方法,这句代码必须放在构造方法的第一句。
执行结果:
无参构造方法
无参构造方法
有参构造方法: sid, name
无参构造方法
有参构造方法: sid, name
有参构造方法: sid, name, age
10 代码块
在类中还有代码块,代码块的主要作用也是进行一些初始化的工作。
代码块有静态代码块和非静态代码块。
静态代码块主要用于 初始化类的静态变量 或执行只需在类加载时运行一次的代码。
非静态代码块主要是对成员变量进行初始化工作。
举个栗子:
class Student {
// 静态变量
public static int staticValue;
// 成员变量
public int value;
// 静态代码块
static {
staticValue = 10;
System.out.println("执行静态代码块");
}
// 非静态代码块
{
value = 20;
System.out.println("执行非静态代码块");
}
// 构造方法
public Student() {
System.out.println("执行构造方法");
}
}
/**
* 测试类
*/
public class ObjectTest {
public static void main(String[] args) {
new Student();
new Student();
}
}
执行结果:
执行静态代码块
执行非静态代码块
执行构造方法
执行非静态代码块
执行构造方法
从执行结果可以看出,静态代码块只会在类加载的时候执行一次,后面不会再执行了。静态代码块在每次创建对象的时候都会执行,而且会在构造方法之前执行。在代码块中也是可以调用类中的方法的,当然静态代码块只能调用静态方法。
静态代码块可以在需要进行一些逻辑处理后,再初始化静态变量的时候会用到,非静态代码块用的不多,一般使用构造方法就可以完成相同的功能。
需要注意,代码块和属性的显式赋值是同级的。谁写在后面,谁后生效。
举个栗子:
class Student {
// 静态变量
public static int staticValue = 20;
// 静态代码块
static {
staticValue = 10;
}
}
class Teacher {
// 静态代码块
static {
staticValue = 10;
}
// 静态变量
public static int staticValue = 20;
}
/**
* 测试类
*/
public class ObjectTest {
public static void main(String[] args) {
System.out.println(Student.staticValue); // 10
System.out.println(Teacher.staticValue); // 20
}
}
上面的 Student 和 Teacher 两个类中的静态变量,分别使用了显式赋值和静态代码块赋值,可以看到它们是同级的,谁在后面谁后执行,覆盖前面的初始化值。
11 属性的初始化顺序
类中的属性,有静态变量和成员变量,我们可以不初始化,会有默认值,也可以显式的进行初始化,也可以在静态代码块中对静态变量进行初始化,也可以构造方法中进行初始化。
这么多地方可以对属性进行初始化,那么初始化顺序是怎么样的呢?
- 静态变量默认初始化;
- 静态变量显式初始化;
- 静态代码块初始化;
- 成员变量默认初始化;
- 成员变量显式初始化;
- 构造方法中初始化;