day8 8 java中的类与对象

8、java中的类与对象

面向对象简称 OO(Object Oriented),20 世纪 80 年代以后,有了面向对象分析(OOA)、 面向对象设计(OOD)、面向对象程序设计(OOP)等新的系统开发方式模型的研究。

对 Java 语言来说,一切皆是对象。把现实世界中的对象抽象地体现在编程世界中,一个对象代表了某个具体的操作。一个个对象最终组成了完整的程序设计,这些对象可以是独立存在的,也可以是从别的对象继承过来的。对象之间通过相互作用传递信息,实现程序开发。

对象的概念
Java 是面向对象的编程语言,对象就是面向对象程序设计的核心。所谓对象就是真实世界中的实体,对象与实体是一一对应的,也就是说现实世界中每一个实体都是一个对象,它是一种具体的概念。对象有以下特点:

  1. 对象具有属性和行为。
  2. 对象具有变化的状态。
  3. 对象具有唯一性。
  4. 对象都是某个类别的实例。

一切皆为对象,真实世界中的所有事物都可以视为对象。

例如,在真实世界的学校里,会有学生和老师等实体,学生有学号、姓名、所在班级等属性(数据),学生还有学习、提问、吃饭和走路等操作。学生只是抽象的描述,这个抽象的描述称为“类”。在学校里活动的是学生个体,即张同学、李同学等,这些具体的个体称为“对象”,“对象”也称为“实例”。

面向对象的三大核心特性
面向对象开发模式更有利于人们开拓思维,在具体的开发过程中便于程序的划分,方便程序员分工合作,提高开发效率。面向对象程序设计有以下优点。

  1. 可重用性:代码重复使用,减少代码量,提高开发效率。下面介绍的面向对象的三大核心特性(继承、封装和多态)都围绕这个核心。
  2. 可扩展性:指新的功能可以很容易地加入到系统中来,便于软件的修改。
  3. 可管理性:能够将功能与数据结合,方便管理。

该开发模式之所以使程序设计更加完善和强大,主要是因为面向对象具有继承、封装和多态 3 个核心特性。
1) 继承性
如同生活中的子女继承父母拥有的所有财产,程序中的继承性是指子类拥有父类的全部特征和行为,这是类之间的一种关系。Java 只支持单继承。

例如定义一个语文老师类和数学老师类,如果不采用继承方式,那么两个类中需要定义的属性和方法如图 1 所示。
在这里插入图片描述

从图 1 能够看出,语文老师类和数学老师类中的许多属性和方法相同,这些相同的属性和方法可以提取出来放在一个父类中,这个父类用于被语文老师类和数学老师类继承。当然父类还可以继承别的类,如图 2 所示。

在这里插入图片描述
图 2 父类继承示例图

总结图 2 的继承关系,可以用概括的树形关系来表示,如图 3 所示。

在这里插入图片描述
图 3 类继承示例图

从图 3 中可以看出,学校主要人员是一个大的类别,老师和学生是学校主要人员的两个子类,而老师又可以分为语文老师和数学老师两个子类,学生也可以分为班长和组长两个子类。

使用这种层次形的分类方式,是为了将多个类的通用属性和方法提取出来,放在它们的父类中,然后只需要在子类中各自定义自己独有的属性和方法,并以继承的形式在父类中获取它们的通用属性和方法即可。

C++ 支持多继承,多继承就是一个子类可有多个父类。例如,客轮是轮船也是交通工具,客轮的父类是轮船和交通工具。多继承会引起很多冲突问题,因此现在很多面向对象的语言都不支持多继承。Java 语言是单继承的,即只能有一个父类,但 Java 可以实现多个接口(接口类似于类,但接口的成员没有执行体。详细了解可参考《Java接口》一节),可以防止多继承所引起的冲突问题。(java通过接口可以实现多继承)

2)封装性
封装是将代码及其处理的数据绑定在一起的一种编程机制,该机制保证了程序和数据都不受外部干扰且不被误用。封装的目的在于保护信息,使用它的主要优点如下。

  1. 保护类中的信息,它可以阻止在外部定义的代码随意访问内部代码和数据。
  2. 隐藏细节信息,一些不需要程序员修改和使用的信息,比如取款机中的键盘,用户只需要知道按哪个键实现什么操作就可以,至于它内部是如何运行的,用户不需要知道。
  3. 有助于建立各个系统之间的松耦合关系,提高系统的独立性。当一个系统的实现方式发生变化时,只要它的接口不变,就不会影响其他系统的使用。例如 U 盘,不管里面的存储方式怎么改变,只要 U 盘上的 USB 接口不变,就不会影响用户的正常操作。
  4. 提高软件的复用率,降低成本。每个系统都是一个相对独立的整体,可以在不同的环境中得到使用。例如,一个 U 盘可以在多台电脑上使用。

**Java 语言的基本封装单位是类。**由于类的用途是封装复杂性,所以类的内部有隐藏实现复杂性的机制。Java 提供了私有和公有的访问模式,类的公有接口代表外部的用户应该知道或可以知道的每件东西,私有的方法数据只能通过该类的成员代码来访问,这就可以确保不会发生不希望的事情。

3)多态性
面向对象的多态性,即“一个接口,多个方法”。多态性体现在父类中定义的属性和方法被子类继承后,可以具有不同的属性或表现方式。多态性允许一个接口被多个同类使用,弥补了单继承的不足。多态概念可以用树形关系来表示,如图 4 所示。

在这里插入图片描述
图 4 多态示例图

从图 4 中可以看出,老师类中的许多属性和方法可以被语文老师类和数学老师类同时使用,这样也不易出错。

8.1 引用类型——类(对比C++)

8.1.1 定义形式差异

java里边定义的类

[public][abstract|final]class<class_name>[extends<class_name>][implements<interface_name>] {
    // 定义属性部分
    <property_type><property1>;
    <property_type><property2>;
    <property_type><property3>;// 定义方法部分
    function1();
    function2();
    function3();}

上述语法中,中括号“[]”中的部分表示可以省略竖线“|”表示“或关系”,例如 abstract|final,说明可以使用 abstract 或 final 关键字,但是两个关键字不能同时出现。

上述语法中各关键字的描述如下。

  1. public:表示“共有”的意思。如果使用 public 修饰,则可以被其他类和程序访问。每个 Java 程序的主类都必须是 public 类,作为公共工具供其他类和程序使用的类应定义为 public 类。
  2. abstract:如果类被 abstract 修饰,则该类为抽象类,抽象类不能被实例化,但抽象类中可以有抽象方法(使用 abstract 修饰的方法)和具体方法(没有使用 abstract 修饰的方法)。继承该抽象类的所有子类都必须实现该抽象类中的所有抽象方法(除非子类也是抽象类)。
  3. final:如果类被 final 修饰,则不允许被继承。
  4. class:声明类的关键字。
  5. extends:表示继承其他类。
  6. implements:表示实现某些接口。

property_type:表示成员变量的类型。
property:表示成员变量名称。
function():表示成员方法名称。

Java 类名的命名规则:

  1. 类名应该以下划线(_)或字母开头,最好以字母开头。
  2. 第一个字母最好大写,如果类名由多个单词组成,则每个单词的首字母最好都大写。
  3. 类名不能为 Java 中的关键字,例如 boolean、this、int 等。
  4. 类名不能包含任何嵌入的空格或点号以及除了下划线(_)和美元符号($)字符之外的特殊字符。

c++里边定义

class Student{//类名前面没有访问权限修饰符
public://相同的成员属性写一次,也可以每次写一个,但是public要重新写
    //成员变量
    char *name;
    int age;
    float score;
    //成员函数
    void say(){
        cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
    }
};//java没有

1.注意在类定义的最后有一个分号;,它是类定义的一部分,表示类定义结束了,不能省略。
2.成员函数可以写类里边,也可以写类的外边,声明必须在类内。在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会。
3.class没有访问权限修饰

还有其他的区别,我们在下面讲。(如析构函数)

8.1.2 类的属性

Java里边
可以在声明成员变量的同时对其进行初始化,如果声明成员变量时没有对其初始化,则系统会使用默认值初始化成员变量。

初始化的默认值如下:

  1. 整数型(byte、short、int 和 long)的基本类型变量的默认值为 0。
  2. 单精度浮点型(float)的基本类型变量的默认值为 0.0f。
    3.双精度浮点型(double)的基本类型变量的默认值为 0.0d。
  3. 字符型(char)的基本类型变量的默认值为 “\u0000”。
  4. 布尔型的基本类型变量的默认值为 false。
  5. 数组引用类型的变量的默认值为 null。如果创建了数组变量的实例,但没有显式地为每个元素赋值,则数组中的元素初始化值采用数组数据类型对应的默认值。
public class Student {
    public String name;    // 姓名
    final int sex = 0;    // 性别:0表示女孩,1表示男孩
    private int age;    // 年龄,默认赋值为 0
}

c++里边
必须通过构造函数赋予初值。构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用初始化列表。

以下的类只给出一部分

class Student{
private:
    char *m_name;
    int m_age;
    float m_score;
public:
    Student(char *name, int age, float score);
    void show();
};
//采用初始化列表
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
    //TODO:
}

8.1.3 this指针

java里边
this 关键字是 Java 常用的关键字,可用于任何实例方法内指向当前对象,也可指向对其调用当前方法的对象,或者在需要当前类型对象引用时使用。其实很类似于C++的this指针

1)但是如果在内部类中需要使用外部类中的对象,这时就需要使用外部类的类名进行限定。

package twlkyao;
 
public class A { 
    public A() { 
        Inner inner = new Inner();
        inner.outer(); // call the inner class's outer method.
        this.outer(); // call A's outer method.
    } 
    
    public void outer() { 
        System.out.println("outer run");
    } 
    class Inner {
        public void outer(){
            System.out.println("inner run");
            A.this.outer(); // 这里就引用了外部类
            System.out.println("--------");
        }
    }
    public static void main(String[] args) {
    	A a = new A();
    }
} 

2)另外,在构造方法中,经常使用this(参数表)来调用参数多的构造方法,并且Java要求在构造方法中,`this(参数表)要出现在任何其他语句之前。this( ) 不能在普通方法中使用,只能写在构造方法中。

public class Circle {
    	private double radius;
    	
    	public Circle(double radius) {
    		this.radius = radius;
    	}
    	
    	public Circle() {
    		this(1.0); //调用有参构造函数,必须出现在最前面,否则报错
    		this.radius = 2.0; 
    	}
    }

c++里边的this

1、this指针的概念
在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。

2、this只能在成员函数中使用(java一样)

3、this指针不能再静态函数中使用(java一样)
静态函数如同静态变量一样,他不属于具体的哪一个对象,静态函数表示了整个类范围意义上的信息,而this指针却实实在在的对应一个对象,所以this指针不能被静态函数使用。

4、this指针的创建
this指针在成员函数的开始执行前构造的,在成员的执行结束后清除。

5、this指针只有在成员函数中才有定义。
创建一个对象后,不能通过对象使用this指针。也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以&this获得),也可以直接使用的。

8.1.4 对象的创建

java里边

在 Java 语言中创建对象分显式创建与隐含创建两种情况。

1)显式创建对象
对象的显式创建方式有 4 种。
1.使用 new 关键字创建对象
这是常用的创建对象的方法,语法格式如下:
类名 对象名 = new 类名();

2.调用 java.lang.Class 或者 java.lang.reflect.Constuctor 类的 newlnstance() 实例方法
在 Java 中,可以使用 java.lang.Class 或者 java.lang.reflect.Constuctor 类的 newlnstance() 实例方法来创建对象,代码格式如下:

java.lang.Class Class 类对象名称 = java.lang.Class.forName(要实例化的类全称);
类名 对象名 = (类名)Class类对象名称.newInstance();

调用 java.lang.Class 类中的 forName() 方法时,需要将要实例化的类的全称(比如 com.mxl.package.Student)作为参数传递过去,然后再调用 java.lang.Class 类对象的 newInstance() 方法创建对象。

3.调用对象的 clone() 方法
该方法不常用,使用该方法创建对象时,要实例化的类必须继承 java.lang.Cloneable 接口。 调用对象的 clone() 方法创建对象的语法格式如下:

类名对象名 = (类名)已创建好的类对象名.clone();

4.调用 java.io.ObjectlnputStream 对象的 readObject() 方法
说明:

  1. 使用 new 关键字或 Class 对象的 newInstance() 方法创建对象时,都会调用类的构造方法。
  2. 使用 Class 类的 newInstance() 方法创建对象时,会调用类的默认构造方法,即无参构造方法。
  3. 使用 Object 类的 clone() 方法创建对象时,不会调用类的构造方法,它会创建一个复制的对象,这个对象和原来的对象具有不同的内存地址,但它们的属性值相同。
  4. 如果类没有实现 Cloneable 接口,就使用 clone。方法会抛java.lang.CloneNotSupportedException 异常,所以应该让类实现 Cloneable 接口。

2)隐式创建对象
例如下面几种情况。

String strName = "strValue",其中的“strValue”就是一个 String 对象,由 Java 虚拟机隐含地创建。

2.字符串的“+”运算符运算的结果为一个新的 String 对象,示例如下:

String str1 = "Hello";
String str2 = "Java";
String str3 = str1+str2;    // str3引用一个新的String对象

3.当 Java 虚拟机加载一个类时,会隐含地创建描述这个类的 Class 实例。

类的加载是指把类的 .class 文件中的二进制数据读入内存中,把它存放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。

无论釆用哪种方式创建对象,Java 虚拟机在创建一个对象时都包含以下步骤:

  1. 给对象分配内存。
  2. 将对象的实例变量自动初始化为其变量类型的默认值。
  3. 初始化对象,给实例变量赋予正确的初始值。
每个对象都是相互独立的,在内存中占有独立的内存地址,并且每个对象都具有自己的生命周期,
当一个对象的生命周期结束时,对象就变成了垃圾,由 Java 虚拟机自带的垃圾回收机制处理。

注意:一个对象要被使用,则对象必须被实例化,如果一个对象没有被实例化而直接调用了对象中的属性或方法,

Student stu = null;//null意味则没有分配空间,对象没有实例化
stu.Name = "李子文";
stu.Sex = true;
stu.Age = 15;
//则程序运行时会出现以下异常:
//Exception in thread "main" java.lang.NullPointerException

在这里插入图片描述

c++里边
明显我们可以使用一下几种方式:

Student a = Student();//栈上创建
Student *c = new Student();//堆上创建
String s = "hedigl";//通过运算符重载

意义上仅两种:
一种是在栈上创建,形式和定义普通变量类似;另外一种是在堆上使用 new 关键字创建,返回一个匿名指针,必须要用一个指针指向它

8.1.5 匿名对象

我们知道创建对象的标准格式如下:

类名称 对象名 = new 类名称();

每次 new 都相当于开辟了一个新的对象,并开辟了一个新的物理内存空间。如果一个对象只需要使用唯一的一次,就可以使用匿名对象,匿名对象还可以作为实际参数传递。

匿名对象就是没有明确的给出名字的对象,是对象的一种简写形式。一般匿名对象只使用一次,而且匿名对象只在堆内存中开辟空间,而不存在栈内存的引用。

public class Person {
    public String name; // 姓名
    public int age; // 年龄
    // 定义构造方法,为属性初始化
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 获取信息的方法
    public void tell() {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }
    public static void main(String[] args) {
        new Person("张三", 30).tell(); // 匿名对象
    }
}

程序运行结果为:
姓名:张三,年龄:30

在以上程序的主方法中可以发现,直接使用了“new Person(“张三”,30)”语句,这实际上就是一个匿名对象,与之前声明的对象不同,此处没有任何栈内存引用它,所以此对象使用一次之后就等待被 GC(垃圾收集机制)回收。

匿名对象在实际开发中基本都是作为其他类实例化对象的参数传递的,在后面的 Java 应用部分的很多地方都可以发现其用法,而且细心的读者可以发现,匿名对象实际上就是个堆内存空间,对象不管是匿名的还是非匿名的,都必须在开辟堆空间之后才可以使用。

8.1.6 对象的销毁、及析构函数

1.对象的销毁

对象使用完之后需要对其进行清除。对象的清除是指释放对象占用的内存。在创建对象时,用户必须使用 new 操作符为对象分配内存。不过,在清除对象时,由系统自动进行内存回收,不需要用户额外处理。这也是 Java 语言的一大特色,某种程度上方便了程序员对内存的管理。

Java 语言的内存自动回收称为垃圾回收(Garbage Collection)机制,简称 GC。垃圾回收机制是指 JVM 用于释放那些不再使用的对象所占用的内存。

Java 语言并不要求 JVM 有 GC,也没有规定 GC 如何工作。不过常用的 JVM 都有 GC,而且大多数 GC 都使用类似的算法管理内存和执行回收操作。具体的垃圾回收实现策略有好多种,在此不再赘述。

注意:C++语言对象是通过 delete 语句手动释放。如果回收内存的任务由程序负责,也就是说必须在程序中显式地进行内存回收,这无疑会增加程序员的负担,而且存在很多弊端。Java 语言对象是由垃圾回收器收集然后释放,程序员不用关系释放的细节。自动内存管理是现代计算机语言发展趋势,例如:C# 语言的垃圾回收,Objective-C 和 Swift 语言的 ARC(内存自动引用计数管理)。

一个对象被当作垃圾回收的情况主要如下两种。

1)对象的引用超过其作用范围。

{    Object o = new Object();    // 对象o的作用范围,超过这个范围对象将被视为垃圾
}

2)对象被赋值为 null。

{
    Object o = new Object();
    o = null;    // 对象被赋值为null将被视为垃圾
}

在 Java 的 Object 类中还提供了一个 protected 类型的 finalize() 方法,因此任何 Java 类都可以覆盖这个方法,在这个方法中进行释放对象所占有的相关资源的操作。

在 Java 虚拟机的堆区,每个对象都可能处于以下三种状态之一。

1)可触及状态:当一个对象被创建后,只要程序中还有引用变量引用它,那么它就始终处于可触及状态。

2)可复活状态:当程序不再有任何引用变量引用该对象时,该对象就进入可复活状态。在这个状态下,垃圾回收器会准备释放它所占用的内存,在释放之前,会调用它及其他处于可复活状态的对象的 finalize() 方法,这些 finalize() 方法有可能使该对象重新转到可触及状态。

3)不可触及状态:当 Java 虚拟机执行完所有可复活对象的 finalize() 方法后,如果这些方法都没有使该对象转到可触及状态,垃圾回收器才会真正回收它占用的内存。

注意:调用 System.gc() 或者 Runtime.gc() 方法也不能保证回收操作一定执行,它只是提高了 Java 垃圾回收器尽快回收垃圾的可能性。

2 析构函数(finalize)

析构方法与构造方法相反,当对象脱离其作用域时(例如对象所在的方法已调用完毕),系统自动执行析构方法。析构方法往往用来做清理垃圾碎片的工作,例如在建立对象时用 new 开辟了一片内存空间,应退出前在析构方法中将其释放。

在 Java 的 Object 类中还提供了一个 protected 类型的 finalize() 方法,因此任何 Java 类都可以覆盖这个方法,在这个方法中进行释放对象所占有的相关资源的操作。

对象的 finalize() 方法具有如下特点:

  1. 垃圾回收器是否会执行该方法以及何时执行该方法,都是不确定的。
  2. finalize() 方法有可能使用对象复活,使对象恢复到可触及状态。
  3. 垃圾回收器在执行 finalize() 方法时,如果出现异常,垃圾回收器不会报告异常,程序继续正常运行。

例如:

protected void finalize() {
    // 对象的清理工作
}

例 1
下面通过一个例子来讲解析构方法的使用。该例子计算从类中实例化对象的个数。

1)Counter 类在构造方法中增值,在析构方法中减值。如下所示为计数器类 Counter 的代码:

public class Counter {
    private static int count = 0;    // 计数器变量
    public Counter() {
        // 构造方法
        this.count++;    // 创建实例时增加值
    }
    public int getCount() {
        // 获取计数器的值
        return this.count;
    }
    protected void finalize() {
        // 析构方法
        this.count--;    // 实例销毁时减少值
        System.out.println("对象销毁");
    }
}

2)创建一个带 main() 的 TestCounter 类对计数器进行测试,示例代码如下:

public class TestCounter {
    public static void main(String[] args) {
        Counter cnt1 = new Counter();    // 建立第一个实例
        System.out.println("数量:"+cnt1.getCount());    // 输出1
        Counter cnt2 = new Counter();    // 建立第二个实例
        System.out.println("数量:"+cnt2.getCount());    // 输出2
        cnt2 = null;    // 销毁实例2
        try {
            System.gc();    // 清理内存
            Thread.currentThread().sleep(1000);    // 延时1000毫秒
            System.out.println("数量:"+cnt1.getCount());    // 输出1
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行后输出结果如下:
数量:1
数量:2
对象销毁
数量:1
由于 finalize() 方法的不确定性,所以在程序中可以调用 System.gc() 或者 Runtime.gc() 方法提示垃圾回收器尽快执行垃圾回收操作。

C++里的析构函数

析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。
如
public class demo{
public:
	~demo(){}//析构函数
}

注意:析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。

析构函数的详细解析

发布了93 篇原创文章 · 获赞 65 · 访问量 5479

猜你喜欢

转载自blog.csdn.net/qq_44861675/article/details/105425542