Java基础教程之面向对象 · 第四讲
- 本节学习目标
- 1️⃣ this 关键字
- 1.1 调用本类属性
- 1.2 调用本类方法
- 1.3 表示当前对象
- 2️⃣ 引用传递
- 2.1 基本概念
- 2.2 实际应用
- 🌾 总结
本节学习目标
- 掌握关键字this的特征以及使用;
- 掌握引用传递分析思维;
1️⃣ this 关键字
在Java中,关键字 this
可能是最复杂的一个,但几乎所有的代码开发都离不开它。在Java中,this
可以用于完成以下三件事情:调用本类属性,调用本类方法以及表示当前对象。下面将详细讲解这三种应用场景。
1.1 调用本类属性
在一个类的定义的方法中可以直接访问类中的属性,但是很多时候有可能会出现方法参数名称与属性名称重复的情况,所以此时就需要利用 “this.属性
” 的形式明确地指明要调用的是类中的属性而不是方法的参数,下面通过代码来验证这一问题。
// 范例 1: 观察程序问题
class Book {
private String title;
private double price;
public Book(String title, double price){
title = title; //原本的目的是希望将构造方法中的title变量内容设置给title 属性
price = price; //原本的目的是希望将构造方法中的 price变量内容设置给price属性
}
// setter、getter略
public String getInfo(){
return "书名:"+ title +",价格:"+ price;
}
}
public class TestDemo {
public static void main(String args[]){
Book book = new Book("Java 开发", 89.2);
System.out.println(book.getInfo());
}
}
程序执行结果:
书名:null,价格:0.0
此程序在Book
类中直接定义了一个构造方法,其目的是希望通过构造方法为 title
和 price
两个属性进行赋值,但最终的结果发现并没有成功赋值,这是因为在 Java 中的变量使用具备 “就近取用
”的原则,在构造方法中已经存在 title
或 price
变量名称,所以如果直接调用 title
或 price
变量将不会使用类中的属性,只会使用方法中的参数(如下图所示),所以此时 Book
类的构造方法是无法为 title
或 price
属性赋值的。
在这种情况下为了可以明确地找到要访问的变量(属于类中的属性),需要在变量前加上 this
, 这样就可以准确地进行属性的标记。
需要注意的是,在访问类属性时一定要加上关键字"this
"。在编写代码时,一些朋友主要关注实现代码功能,而忽视了良好的编程习惯。在这里,需要提醒大家的是,为了避免不必要的麻烦,在类中访问属性时无论是否有重名变量,都应该使用关键字"this
"来明确指定。
// 范例 2: 使用 this 关键字明确地表示访问类中的属性
class Book {
private String title;
private double price;
public Book(String title, double price){
this.title = title; //this.属性表示的是本类属性,这样即使与方法中的参数重名也可以明确定位
this.price = price; //this.属性表示的是本类属性,这样即使与方法中的参数重名也可以明确定位
}
// setter、getter 略
public String getInfo(){
return "书名:" + title + ",价格:" + price;
}
}
public class TestDemo{
public static void main(String args[]){
Book book = new Book("Java 开发",89.2);
System.out.println(book.getInfo());
}
}
程序执行结果:
书名:Java 开发,价格:89.2
本程序由于构造方法中访问属性时增加了 this
关键字,所以可以在变量名称相同的情况下,明确地区分属性或参数,传递的内容也可以正常赋值。
1.2 调用本类方法
通过上边的讲解,大家应该清楚了 this
本质上指的就是明确进行本类结构的标记,而除了访问类中的属性外,也可以进行类中方法的调用。
在一个类中,通过this
调用本类方法分为以下两种形式。
- 调用本类普通方法:如果要调用本类方法,则可以使用 “
this. 方法()
”调用; - 调用本类构造方法:在一个构造中要调用其他构造,可以使用 “
this()
” 调用。
// 范例 3: 调用本类普通方法
class Book{
private String title;
private double price;
public Book(String title, double price){
this.title = title:
this.price = price;
}
public void print(){
System.out.println("更多文章请访问:https://blog.csdn.net/LVSONGTAO1225");
}
// setter、getter 略
public String getInfo(){
this.print(); //调用本类方法
return "书名:"+title+",价格:"+price;
}
}
public class TestDemo {
public static void main(String args[]){
Book book =new Book("Java 开发",89.2);
System.out.println(book.getlnfo());
}
}
程序执行结果:
更多文章请访问:https://blog.csdn.net/LVSONGTAO1225
书名:Java开发,价格:89.2
此程序在 getlnfo()
方法执行时,进行对象信息取得前,调用了本类中定义的 print()
方法,由于是在本类定义的,所以可以直接利用 “this.方法()
” 的形式进行访问。
从代码的严格角度来讲,利用“this.方法()
”调用本类中其他普通方法的做法是非常标准的,由于是在一个类中,也可以直接使用“方法()
”的形式调用。但是从代码标准的角度来说都会使用 this
调用本类属性或本类方法。
除了可以调用本类普通方法,在一个类中也可以利用 “this()
” 的形式实现一个类中多个构造方法的互相调用。例如:一个类中存在3个构造方法 (无参,有一个参数,有两个参数), 但是不管使用何种构造方法,都要求在实例化对象产生的时候输出一行提示信息:“一个新 Book 类对象产生了”,则按照前面学习到的知识,可以编写如下代码来实现。
// 范例 4: 观察程序问题
class Book {
private String title;
private double price;
public Book(){
System.out.println("一个新 Book 类对象产生了");
}
public Book(String title){
System.out.println("一个新 Book 类对象产生了");
this.title = title;
)
public Book(String title, double price){
System.out.println("一个新 Book 类对象产生了");
this.title = title;
this.price = price;
}
// setter、getter 略
public String getInfo(){
return "书名:"+this.title+",价格:"+this.price;
}
}
public class TestDemo{
public static void main(String args[]){
Book book = new Book("Java 开发",89.2);
System.out.println(book.getInfo());
}
}
程序执行结果:
一个新 Book 类对象产生了
书名:Java 开发,价格:89.2
此程序定义了3个构造方法,并且每一个构造方法都会打印相应的信息提示。此时,不管调用哪一个构造方法,都可以完成指定的信息输出。可以发现,如果假设将这一行输出的代码想象为50行代码量,那么这个程序中会出现大量的重复代码,而程序的设计目标是减少重复代码,那么此时就可以利用 this()
来避免重复代码。
// 范例 5: 消除构造方法中的重复代码
class Book{
private String title;
private double price;
public Book(){
System.out.println("一个新 Book 类对象产生了"); //把这行语句想象成50行代码
}
public Book(String title){
this(); //调用本类无参构造方法
this.title = title;
}
public Book(String title, double price)(
this(title); //调用本类有一个参数的构造方法
this.price = price;
}
// setter、getter 略
public String getInfo(){
return "书名:"+this.title+",价格:"+this.price;
}
}
public class TestDemo {
public static void main(String args[]){
Book book = new Book("Java开发",89.2);
System.out.println(book.getInfo());
}
}
程序执行结果:
一个新 Book 类对象产生了
名:Java开发,价格:89.2
此程序在两个参数的构造方法中(“public Book(String title, double price)
”)使用 “this(title)
” 调用了有一个参数的构造方法(“public Book(String title)
”), 而在一个参数的构造中调用了无参构造,这样不管调用哪个构造,都会执行特定功能(案例为输出操作)。
但需要注意的是,在使用 this
调用构造方法时,存在两个重要的限制(会在程序编译时检查出来):
- 使用"
this()
" 调用构造方法形式的代码只能够放在构造方法的首行; - 进行构造方法互相调用时,一定要保留调用的出口。
对于第一点限制比较好理解,因为构造方法是在类对象实例化时调用的,所以构造方法间的互相调用,就只能放在构造方法中编写。如果我们将 “this()
” 调用放置在构造方法的非首行位置,那么在该语句之前已经有一些初始化操作被执行了。这就会导致调用的构造方法中的初始化操作无法准确地进行,从而产生错误或不符合预期的结果。
因此,为了确保对象的正确初始化顺序和避免出现意外的问题,使用 "this()
"调用构造方法的代码必须放在构造方法的首行。这样可以保证先执行其他构造方法进行必要的初始化,然后再继续执行该构造方法中的特定初始化操作,确保对象状态的正确性。
进行构造方法互相调用时,一定要保留调用的出口是为了确保程序的逻辑正确性和避免可能的死循环。
当一个构造方法调用另一个构造方法时,实际上是在进行对象的初始化过程。这种调用可以使代码更加简洁和灵活,方便我们根据需要选择不同的构造方法来进行对象初始化。
然而,如果我们不小心将调用的出口删除或忽略,就会导致构造方法间的无限循环调用(也称为递归调用),从而导致程序进入无限循环并最终导致堆栈溢出错误。
通过保留构造方法调用的出口,即使用关键字"this()
"来明确指示调用其他构造方法,我们可以确保每个构造方法都有明确定义的结束点,从而防止无限循环的发生。
下面通过代码来验证,辅助理解第二个限制。
// 范例 6: 观察错误的代码
class Book {
private String title
private double price;
public Book(){
this("HELLO",1.1); //调用双参构造
System.out.println("一个新的Book类对象产生。");
}
public Book(String title){
this();
this.title = title;
}
public Book(String title, double price){
this(title); //调用本类的单参构造
this.price = price;
}
// setter、getter略
public String getInfo(){
return "书名:"+this.title+",价格:"+this.price;
}
}
此时调用的语句的确是满足了需要放在构造方法的首行的第一点限制 , 但是可以发现 , 此时会形成一个构造方法间互相调用的死循环状态。 所以在编译时就会出现错误提示 "构造方法递归调用
" , 也就是说在使用 this 调用构造方法时 ,一定要留有出口,不允许出现循环调用的情况。
下边通过一个实例演示 this
调用构造方法的正确使用:
定义一个雇员类(编号、姓名、工资、部门), 在这个类里提供了以下4个构造方法。
- 无参构造:编号为0,姓名为无名氏,工资为0.0,部门设置为“未定”;
- 单参构造(传递编号):姓名为“临时工”,工资为800.0,部门为后勤;
- 双参构造(传递编号、姓名):工资为2000.0,部门为技术部;
- 全参构造。
// 范例 7 : 利用构造方法互相调用简化代码
class Emp {
private int empno;
private String ename;
private double sal;
private String dept;
public Emp(){
this(0,"无名氏",0.0,"未定"); //调用全参构造
}
public Emp(int empno){
this(empno,"临时工",800.0,"后勤部"); //调用全参构造
}
public Emp(int empno, String ename){
this(empno, ename, 2000.0,"技术部"); //调用全参构造
}
public Emp(int empno, String ename, double sal, String dept){
this.empno = empno;
this.ename = ename;
this.sal = sal;
this.dept = dept;
)
public String getInfo(){
return "雇员编号:"+this.empno+", 姓名:"+this.ename+", 工资:"+this.sal +",部门:"+ this.dept;
}
// setter、getter略
}
此案例通过“this()
”语法的形式,很好地解决了构造方法中代码重复的问题。
1.3 表示当前对象
this
关键字在应用的过程中有一个最为重要的概念——当前对象,而所谓的当前对象指的就是当前正在调用类中方法的实例化对象,下面直接通过一个代码案例来观察。
// 范例 8: 直接输出对象
class Book {
public void print(){ //调用 print()方法的对象就是当前对象,this就自动与此对象指向同一块内存地址
System.out.println("this = " + this); // this就是当前调用方法的对象
}
}
public class TestDemo{
public static void main(String args[]){
Book booka = new Book(); //实例化新的 Book 类对象
System.out.printin("booka = "+ booka); //主方法中输出 Book类对象
booka.print(); //调用 Book 类的print()方法输出,此时booka 为当前对象
System.out.printin("---------------------------");
Book bookb = new Book(); //实例化新的Book 类对象
System.out.println("bookb = "+ bookb); //主方法中输出Book类对象
bookb.print(); //调用 Book类的print)方法输出,此时bookb 为当前对象
}
}
程序执行结果:
booka = Book@1db9742
this = Book@ldb9742
bookb = Book@106d69c
this = Book@106d69c
此程序首先实例化了两个 Book
类对象,然后在主方法中直接进行对象信息的输出。可以发现每个对象都有一个独立的对象编码,而在使用不同的对象调用 print()
方法时, this
的引用也会发生改变,即this
为当前对象。
清楚了 this
表示当前对象这一基本特征后,实际上在之前出现的 “this.属性
”就属于当前对象中的属性,也就是堆内存中保存的内容。
2️⃣ 引用传递
引用传递是Java中一个让初学者感到费解的概念,但在实际开发中却具有非常重要的作用。为了更好地理解引用传递的相关概念,本节将对引用传递的使用进行全面分析,并详细讲解它在实际开发中的操作意义。
2.1 基本概念
在Java中,基本类型的变量是直接存储值的,而引用类型的变量存储的是对象的引用(内存地址)。当我们把一个引用类型的变量作为参数传递给方法时,实际上是将该引用的副本传递给方法。这意味着方法内部的操作会影响原始对象,因为它们引用同一个对象。
引用传递核心意义是:同一块堆内存空间可以被不同的栈内存所指向,不同栈内存可以对同一堆内存进行内容的修改。通过引用传递,我们可以在方法中修改对象的状态,使得代码更加灵活和易于维护。例如,在集合类中添加或删除元素、修改对象属性等操作都是基于引用传递实现的。
然而,理解引用传递也需要注意一些细节。因为只是传递了引用的副本,重新为参数赋值并不会改变原始的引用,仅在当前方法有效。此外,当将引用作为参数传递给其他方法时,方法之间共享的是同一个对象。这意味着,对于共享的对象进行修改会影响到所有引用该对象的地方。
// 范例 9: 引用传递
class Message {
private int num=10; //定义int基本类型的属性
/**
* 本类没有提供无参构造方法,而是提供有参构造,可以接收num 属性的内容
*@param num 接收num 属性的内容
*/
public Message(int num){
this.num = num; //为num 属性赋值
}
public void setNum(int num){
this.num = num;
}
public int getNum(){
return this.num;
}
}
public class TestDemo {
public static void main(String args[]){
Message msg = new Message(30); //实例化Message 类对象同时传递num 属性内容
fun(msg); //引用传递
System.out.println(msg.getNum()); //输出num 属性内容
}
/**
*修改Message 类中的num 属性内容
*@param temp Message 类的引用
*/
public static void fun(Message temp){
temp.setNum(100); //修改num 属性内容
}
}
程序执行结果:
100
此程序首先在 Message
类中只定义了一个 num
属性(int
基本数据类型), 然后利用 Message
类的构造方法进行 num
属性的赋值操作,最后在主方法中调用了 fun()
方法,在方法的参数上接收了 Message
类对象的引用,以便可以修改num
属性的内容。而当 fun()
方法执行完毕后 temp
断开与堆内存的引用,但是对于堆内存的修改却保存了下来,所以最终的结果是100
。此程序的内存关系如下图所示。
上边这个引用范例是一个标准的引用传递操作,即不同的栈内存指向了同一块堆内存空间,但是在进行引用分析中不得不去考虑一种特殊的类——String
类。
// 范例 10: 引用传递
public class TestDemo{
public static void main(String args[]){
String msg = "Hello"; //定义String类对象
fun(msg); //引用传递
System.out.println(msg); //输出msg 对象内容
}
public static void fun(String temp){ //接收字符串引用
temp = "World"; //改变字符串引用
}
}
程序执行结果:
Hello
此程序首先定义了一个 String
类的对象,内容为 “Hello
”, 然后将此对象的引用传递给 fun()
方法中的 temp
, 即两个 String 类对象都指向着同一个堆内存空间,但是由于 String 对象内容的变化是通过引用的改变实现的,所以在 fun()
方法中所做的任何修改都不会影响到原本的 msg
对象内容,最终的结果依然是“Hello
”。本程序的内存分析如下图所示。
此程序中,因为字符串内容不可改变,String 类对象内容的改变是通过引用的变更实现的,所以 temp = "World";
实际上是创建了一个新的匿名String对象,但是所有变更都是在 fun()
方法中完成的, 当 fun()
方法执行完毕 temp
将失效,其对应的堆内存空间也将成为垃圾。
以上两个程序范例,第一个利用了 Message
类的对象实现引用传递,而第二个直接利用 String
类对象实现引用,接下来将这两个范例结合在一起来观察引用传递。
// 范例 11: 引用传递
class Message {
private String info = "xiaoshan"; //定义String类型属性
public Message(String info){ //利用构造方法设置info属性内容
this.info = info;
}
public void setInfo(String info){
this.info = info;
}
public String getInfo(){
return this.info;
}
}
public class TestDemo {
public static void main(String args[]){
Message msg = new Message("Hello"); //实例化Message 类对象
fun(msg); //引用传递
System.out.println(msg.getInfo()); //输出info属性内容
}
public static void fun(Message temp){ //接收Message 类引用
temp.setInfo("World"); //修改info属性内容
}
}
程序执行结果:
World
此程序首先在 Message
类中定义了一个 String
类型的 info
属性,然后在主方法中实例化 Message
类对象 msg
, 最后将此对象传递到 fun()
方法中。此时 temp
与 msg
将具备同一块堆内存空间的引用,而在 fun()
方法中修改了指定空间的 info
属性内容,所以最终的 info
的结果为 “World
”。此程序的内存关系如下图所示。
实际上,上图所示的内存分析流程与上边利用了 Message
类的对象实现引用传递的第一个范例区别不大,唯一的区别是将 int
数据类型的属性替换为了 String
数据类型的属性,由于此时 info
属性是定义在 Message
类中的,所以在 fun()
方法中对 info
的修改可以被保存下来。
2.2 实际应用
面向对象是一种可以抽象化描述现实社会事物的编程思想,理论上现实生活中的一切都可以进行合理的抽象。下面实现这样一种类的设计:假如每一个人都有一辆汽车或都没有汽车。很明显,人应该是一个类,而车也应该是一个类,人应该包含一个车的属性,而反过来车也应该包含一个人的属性,面对这样的关系就可以采用下图所示的引用方式来实现。
// 范例 12: 引用传递 代码实现(无参构造、setter、getter 略,同时本程序定义的是两个简单Java类)
class Member {
private int mid; //人员编号
private String name; //人员姓名
private Car car; //表示属于人的车,如果没有车则内容为null
public Member(int mid,String name){
this.mid = mid;
this.name = name;
}
public void setCar(Car car){
this.car = car;
}
public Car getCar(){
return this.car;
}
public String getInfo(){
return "人员编号:"+this.mid+", 姓名:"+this.name;
}
}
class Car{
private Member member; //车属于一个人,如果没有所属者,则为null
private String pname; //车的名字
public Car(String pname){
this.pname = pname;
}
public void setMember(Member member){
this.member = member;
}
public Member getMember(){
return this.member;
}
public String getInfo(){
return "车的名字:"+ this.pname;
}
}
此程序类设计类完成后,需要对程序进行测试,程序的测试要求主要分为以下两步:根据定义的结构关系设置数据、根据定义的结构关系取出数据。
// 范例 13: 代码测试
public class TestDemo{
public static void main(String args[]){
//第一步:根据既定结构设置数据
Member member = new Member(1,"小山"); //独立对象
Car car = new Car("会漂移的五菱宏光"); //独立对象
member.setCar(car); //一个人有一辆车
car.setMember(member); //一辆车属于一个人
//第二步:根据既定结构取出关系
System.out.println(member.getCar().getInfo()); //通过人找到车的信息
System.out.println(car.getMember().getInfo()); //通过车找到人的信息
}
}
程序执行结果:
车的名字:会漂移的五菱宏光
人员编号:1, 姓名:小山
此程序首先实例化了Member
与 Car
类各自的对象,然后分别利用各自类中的 setter
方法设置了对象间的引用关系。
这种一对一关系是一种相对比较容易的操作。下面可以进一步设计,例如:每个人都有自己的孩子,孩子还可能有车,那么有如下两种设计方法。
方法一:设计一个表示孩子的类;
存在问题:如果有后代就需要设计一个类,按照这样的思路,如果有孙子,则应该再来个孙子类,如果有曾孙,再来个曾孙类,并且这些类的结构都是完全一样的,这样的设计有些糟糕。
方法二: 一个人的孩子一定还是一个人,与人的类本质没区别,可以在 Member
类里面设计一个属性,表示孩子,其类型也是 Member
。
// 范例 14: 修改Member类定义
class Member {
private int mid; //人员编号
private String name; //人员姓名
private Car car; //表示属于人的车,如果没有车,则内容为 null
private Member child; //表示人的孩子,如果没有,则为null
public Member(int mid, String name){
this.mid = mid;
this.name = name;
}
public void setCar(Car car){
this.car = car;
}
public Car getCar(){
return this.car;
}
public void setChild(Member child){
this.child = child;
}
public Member getChild(){
return child;
}
public String getInfo(){
return "人员编号:"+this.mid+", 姓名:"+ this.name;
}
}
class Car{
private Member member; //车属于一个人,如果没有所属者,则为null
private String pname; //车的名字
public Car(String pname){
this.pname = pname;
}
public void setMember(Member member){
this.member = member;
}
public Member getMember(){
return this.member;
}
public String getInfo(){
return "车的名字:"+this.pname;
}
}
public class TestDemo{
public static void main(String args[]){
//第一步:根据既定结构设置数据
Member member = new Member(1,"小山"); //独立对象
Member child = new Member(2,"福满多"); //独立对象
Car car1 = new Car("双肾SUV"); //一辆车
Car car2 = new Car("玛莎拉蒂");
member.setCar(car1); //一个人有一辆车
car1.setMember(member); //一辆车属于一个人
child.setCar(car2); //一个孩子有一辆车
car2.setMember(child); //一个车属于一个孩子
member.setChild(child); //一个人有一个孩子
//第二步:根据既定结构取出关系
System.out.println(member.getCar().getInfo()); //通过人找到车的信息
System.out.printin(car1.getMember().getInfo()); //通过车找到人的信息
System.out.println(member.getChild().getInfo()); //通过人找到他孩子的信息
System.out.println(member.getChild().getCar().getInfo()); //通过人找到他孩子的车的信息
}
}
程序执行结果:
车的名字:双肾SUV
人员编号:1, 姓名:小山
人员编号:2, 姓名:福满多
车的名字:玛莎拉蒂
此程序在 Member
类中增加了一个表示孩子的属性 “child
”, 其类型为 Member
类型,如果一个人有孩子则为其设置一个实例化对象,如果没有则设置为 null
。特别需要注意的是,在通过人找到孩子所对应车的信息时使用了代码链的形式 " member.getChild().getCar().getlnfo()
", 这类代码一定要观察每一个方法的返回值,如果返回的是一个类的引用,则可以继续调用这个类的方法,而此类代码在以后的开发中也会出现。
🌾 总结
本文详细介绍了Java中的关键字this
以及引用传递的基本概念和实际应用。通过学习this
关键字,我们了解到它能够完成调用本类属性、调用本类方法和表示当前对象的功能。在编写代码时,为了避免不必要的麻烦和确保代码的正确性,我们建议在访问类属性时一定要加上this
关键字。
同时,我们也探讨了引用传递的基本概念和作用。在Java中,引用传递允许我们将对象的引用副本传递给方法,使得方法内部的操作能够影响到原始对象。这种特性在实际开发中非常重要,可以实现对对象状态的修改和共享的功能,提高代码的可读性和可维护性。
了解和运用this
关键字和引用传递对于Java程序员来说至关重要。通过灵活地使用this
关键字,我们可以更好地操作和管理对象,避免命名冲突和提高代码的可读性。而引用传递的理解和应用可以帮助我们处理对象之间的关系,实现对象之间的相互作用。
在编写Java代码时,深入理解和熟练运用 this
关键字和引用传递是提高代码质量和效率的重要一环。