Java面向对象

2年前 (2022) 程序员胖胖胖虎阿
179 0 0

一、思想

1、要理解面向对象思想,我们先要知道什么是对象?

《Java编程思想》中提到“万物皆为对象”的概念。它将对象视为一种奇特的变量,它除了可以存储数据之外还可以对它自身进行操作。它能够直接反映现实生活中的事物,例如人、车、小鸟等,将其表示为程序中的对象。每个对象都具有各自的状态特征(也可以称为属性)及行为特征(方法),java就是通过对象之间行为的交互来解决问题的。

面向对象就是把构成问题的事物分解成一个个对象,建立对象不是为了实现一个步骤,而是为了描述某个事物在解决问题中的行为。

类是面向对象中的一个很重要的概念,因为类是很多个具有相同属性和行为特征的对象所抽象出来的,对象是类的一个实例。

类具有三个特性:封装、继承和多态。

  • 封装:核心思想就是“隐藏细节”、“数据安全”,将对象不需要让外界访问的成员变量和方法私有化,只提供符合开发者意愿的公有方法来访问这些数据和逻辑,保证了数据的安全和程序的稳定。
  • 继承:子类可以继承父类的属性和方法,并对其进行拓展。
  • 多态:同一种类型的对象执行同一个方法时可以表现出不同的行为特征。通过继承的上下转型、接口的回调以及方法的重写和重载可以实现多态。

2、对象之间的关系

1、依赖

可以简单的理解,就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A;比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖;表现在代码层面,为类B作为参数被类A在某个method方法中使用;

2、关联

他体现的是两个类、或者类与接口之间语义级别的一种强依赖关系,比如我和我的朋友;这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的、关联可以是单向、双向的;表现在代码层面,为被关联类B以类属性的形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量;

3、聚合

聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与CPU、公司与员工的关系等;表现在代码层面,和关联关系是一致的,只能从语义级别来区分; 

4、组合

组合也是关联关系的一种特例,他体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合;他同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束;比如你和你的大脑;表现在代码层面,和关联关系是一致的,只能从语义级别来区分; 

二、原则

1、继承

继承的概念:

继承是类与类的一种关系,是一种“is a”的关系。比如“狗”继承“动物”,这里动物类是狗类的父类或者基类,狗类是动物类的子类或者派生类。如下图所示:

Java面向对象

注:java中的继承是单继承,即一个类只有一个父类。 

继承的好处:

子类拥有父类的所有属性和方法(除了private修饰的属性不能拥有)从而实现了实现代码的复用; 

语法规则:

只要在子类加上extends关键字继承相应的父类就可以了:

Java面向对象


A、方法的重写 

子类如果对继承的父类的方法不满意(不适合),可以自己编写继承的方法,这种方式就称为方法的重写。当调用方法时会优先调用子类的方法。

重写要注意:

        a、返回值类型

        b、方法名

        c、参数类型及个数

都要与父类继承的方法相同,才叫方法的重写。

重载和重写的区别:

        方法重载:在同一个类中处理不同数据的多个相同方法名的多态手段。

        方法重写:相对继承而言,子类中对父类已经存在的方法进行区别化的修改。

B、继承的初始化顺序

1、初始化父类再初始化子类

2、先执行初始化对象中属性,再执行构造方法中的初始化。

基于上面两点,我们就知道实例化一个子类,java程序的执行顺序是:

父类对象属性初始化---->父类对象构造方法---->子类对象属性初始化--->子类对象构造方法   

下面有个形象的图:

Java面向对象

C、final关键字

使用final关键字做标识有“最终的”含义。

        1. final 修饰类,则该类不允许被继承。

        2. final 修饰方法,则该方法不允许被覆盖(重写)

        3. final 修饰属性,则该类的该属性不会进行隐式的初始化,所以 该final 属性的初始化属性必须有值,或在构造方法中赋值(但只能选其一,且必须选其一,因为没有默认值!),且初始化之后就不能改了,只能赋值一次

        4. final 修饰变量,则该变量的值只能赋一次值,在声明变量的时候才能赋值,即变为常量

D、super关键字

在对象的内部使用,可以代表父类对象。

        1、访问父类的属性:super.age

        2、访问父类的方法:super.eat()

super的应用:

        首先我们知道子类的构造的过程当中必须调用父类的构造方法。其实这个过程已经隐式地使用了我们的super关键字。

        这是因为如果子类的构造方法中没有显示调用父类的构造方法,则系统默认调用父类无参的构造方法。

        那么如果自己用super关键字在子类里调用父类的构造方法,则必须在子类的构造方法中的第一行

        要注意的是:如果子类构造方法中既没有显示调用父类的构造方法,而父类没有无参的构造方法,则编译出错。

(补充说明,虽然没有显示声明父类的无参的构造方法,系统会自动默认生成一个无参构造方法,但是,如果你声明了一个有参的构造方法,而没有声明无参的构造方法,这时系统不会动默认生成一个无参构造方法,此时称为父类有没有无参的构造方法。)

E、Object类

Object类是所有类的父类,如果一个类没有使用extends关键字明确标识继承另一个类,那么这个类默认继承Object类。

Object类中的方法,适合所有子类!!!

那么Object类中有什么主要的方法呢?

        1、toString()

        a. 在Object类里面定义toString()方法的时候返回的对象的哈希code码(对象地址字符串)。

        我们可以发现,如果我们直接用System.out.print(对象)输出一个对象,则运行结果输出的是对象的对象地址字符串,也称为哈希code码。如:

Java面向对象

        哈希码是通过哈希算法生成的一个字符串,它是用来唯一区分我们对象的地址码,就像我们的身份证一样。  

        b. 可以通过重写toString()方法表示出对象的属性。

        如果我们希望输出一个对象的时候,不是它的哈希码,而是它的各个属性值,那我们可以通过重写toString()方法表示出对象的属性。

        2、equals()

        a、equals()----返回值是布尔类型。

        b、默认的情况下,比较的是对象的引用是否指向同一块内存地址-------对象实例化时,即给对象分配内存空间,该内存空间的地址就是内存地址。使用方法如:dog.equals(dog2);

        c、 如果是两个对象,但想判断两个对象的属性是否相同,则重写equals()方法。

        以Dog类为例,重写后的equals()方法如下(当然你可以根据自己想比较的属性来重写,这里我以age属性是否相同来重写equals()方法):

Java面向对象

上面有四个判断,它们的含义分别是:

        1.判断地址是否相同----if (this == obj),相同则返回true

        2.判断对象是否为空----if (obj == null),为空则返回false

        3.getClass()可以得到类对象,判断类型是否一样-----if (getClass() != obj.getClass()),不一样则返回false

        4.判断属性值是否一样----if (age != other.age),不一样返回false

        5.如果地址相同,对象不为空,类型一样,属性值一样则返回true

        这里要注意的是,理解obj.getClass()得到的类对象和类的对象的区别,以下用图形表示:

Java面向对象

可以看到,对于类对象我们关心它属于哪个类,拥有什么属性和方法,比如我和你都是属于“人”这个类对象;而类的对象则是一个类的实例化的具体的一个对象。比如我和你是两个不同的人。 

 2、封装

1、概念:

将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。

2、好处:

  • 只能通过规定的方法访问数据。
  • 隐藏类的实例细节,方便修改和实现。

3、封装的实现步骤

Java面向对象

需要注意:对封装的属性不一定要通过get/set方法,其他方法也可以对封装的属性进行操作。当然最好使用get/set方法,比较标准。


A、访问修饰符

Java面向对象

从表格可以看出从上到下封装性越来越差。 

B、this关键字

1.this关键字代表当前对象

        this.属性 操作当前对象的属性

        this.方法 调用当前对象的方法。

2.封装对象的属性的时候,经常会使用this关键字。

3.当getter和setter函数参数名和成员函数名重合的时候,可以使用this区别。如:

Java面向对象

C、Java 中的内部类 

内部类( Inner Class )就是定义在另外一个类里面的类。与之对应,包含内部类的类被称为外部类。

那么问题来了:那为什么要将一个类定义在另一个类里面呢?清清爽爽的独立的一个类多好啊!!

答:内部类的主要作用如下:

        1. 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。

        2. 内部类的方法可以直接访问外部类的所有数据,包括私有的数据

        3. 内部类所实现的功能使用外部类同样可以实现,只是有时使用内部类更方便。

内部类可分为以下几种: 

  • 成员内部类
  • 静态内部类
  • 方法内部类
  • 匿名内部类

3、多态

面向对象的最后一个特性就是多态,那么什么是多态呢?多态就是对象的多种形态。

java里的多态主要表现在两个方面:

1.引用多态

  父类的引用可以指向本类的对象;

  父类的引用可以指向子类的对象;

这两句话是什么意思呢,让我们用代码来体验一下,首先我们创建一个父类Animal和一个子类Dog,在主函数里如下所示:

Java面向对象

注意:我们不能使用一个子类的引用来指向父类的对象,如:Java面向对象 

这里我们必须深刻理解引用多态的意义,才能更好记忆这种多态的特性。为什么子类的引用不能用来指向父类的对象呢?我在这里通俗给大家讲解一下:就以上面的例子来说,我们能说“狗是一种动物”,但是不能说“动物是一种狗”,狗和动物是父类和子类的继承关系,它们的从属是不能颠倒的。当父类的引用指向子类的对象时,该对象将只是看成一种特殊的父类(里面有重写的方法和属性),反之,一个子类的引用来指向父类的对象是不可行的!! 

2.方法多态

根据上述创建的两个对象:本类对象和子类对象,同样都是父类的引用,当我们指向不同的对象时,它们调用的方法也是多态的。

创建本类对象时,调用的方法为本类方法;

创建子类对象时,调用的方法为子类重写的方法或者继承的方法;

使用多态的时候要注意:如果我们在子类中编写一个独有的方法(没有继承父类的方法),此时就不能通过父类的引用创建的子类对象来调用该方法!!!

注意: 继承是多态的基础。


A、引用类型转换

了解了多态的含义后,我们在日常使用多态的特性时经常需要进行引用类型转换。

引用类型转换:

1. 向上类型转换(隐式/自动类型转换),是小类型转换到大类型。

就以上述的父类Animal和一个子类Dog来说明,当父类的引用可以指向子类的对象时,就是向上类型转换。如:

Java面向对象

 2. 向下类型转换(强制类型转换),是大类型转换到小类型(有风险,可能出现数据溢出)

将上述代码再加上一行,我们再次将父类转换为子类引用,那么会出现错误,编译器不允许我们直接这么做虽然我们知道这个父类引用指向的就是子类对象,但是编译器认为这种转换是存在风险的如:

Java面向对象

那么我们该怎么解决这个问题呢,我们可以在animal前加上(Dog)来强制类型转换。如:

Java面向对象

但是如果父类引用没有指向该子类的对象,则不能向下类型转换,虽然编译器不会报错,但是运行的时候程序会出错,如:

Java面向对象

其实这就是上面所说的子类的引用指向父类的对象,而强制转换类型也不能转换!!

还有一种情况是父类的引用指向其他子类的对象,则不能通过强制转为该子类的对象。如:

Java面向对象

这是因为我们在编译的时候进行了强制类型转换,编译时的类型是我们强制转换的类型,所以编译器不会报错,而当我们运行的时候,程序给animal开辟的是Dog类型的内存空间,这与Cat类型内存空间不匹配,所以无法正常转换。这两种情况出错的本质是一样的,所以我们在使用强制类型转换的时候要特别注意这两种错误!!下面有个更安全的方式来实现向下类型转换

3. instanceof运算符,来解决引用对象的类型,避免类型转换的安全性问题。

instanceof是Java的一个二元操作符,和==,>,<是同一类东东。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。

我们来使用instanceof运算符来规避上面的错误,代码修改如下:

Java面向对象

利用if语句和instanceof运算符来判断两个对象的类型是否一致。 

补充说明:在比较一个对象是否和另一个对象属于同一个类实例的时候,我们通常可以采用instanceof和getClass两种方法通过两者是否相等来判断,但是两者在判断上面是有差别的。Instanceof进行类型检查规则是:你属于该类吗?或者你属于该类的派生类吗?而通过getClass获得类型信息采用==来进行检查是否相等的操作是严格的判断,不会存在继承方面的考虑

总结:在写程序的时候,如果要进行类型转换,我们最好使用instanceof运算符来判断它左边的对象是否是它右边的类的实例,再进行强制转换。

B、抽象类

定义:

抽象类前使用abstract关键字修饰,则该类为抽象类。

使用抽象类要注意以下几点:

        1. 抽象类是约束子类必须有什么方法,而并不关注子类如何实现这些方法。

        2. 抽象类应用场景:

        a. 在某些情况下,某个父类只是知道其子类应该包含怎样的方法,但无法准确知道这些子类如何实现这些方法(可实现动态多态)。

        b. 从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为子类的模板,从而避免子类设计的随意性。

        3. 抽象类定义抽象方法,只有声明,不需要实现。抽象方法没有方法体以分号结束,抽象方法必须用abstract关键字来修饰。如:

Java面向对象

         4、包含抽象方法的类是抽象类。抽象类中可以包含普通的方法,也可以没有抽象方法。如:

Java面向对象

        5、抽象类不能直接创建,可以定义引用变量来指向子类对象,来实现抽象方法。以上述的Telephone抽象类为例:

public abstract class Telephone {
    public abstract void call();//抽象方法,方法体以分号结束,只有声明,不需要实现
    public void message(){
        System.out.println("我是抽象类的普通方法");
    }//抽象类中包含普通的方法
}
public class Phone extends Telephone {

    public void call() {//继承抽象类的子类必须重写抽象方法
        // TODO Auto-generated method stub
        System.out.println("我重写了抽象类的方法");
    }

}

 以上是Telephone抽象类和子类Phone的定义,下面我们看main函数里:

Java面向对象

 运行结果(排错之后):

Java面向对象

 三、四个常考关键字

1、static

一.static关键字的用途

在《Java编程思想》P86页有这样一段话:

        “static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。”

        这段话虽然只是说明了static方法的特殊之处,但是可以看出static关键字的基本作用,简而言之,一句话来描述就是:

        方便在没有创建对象的情况下来进行调用(方法/变量)。

        很显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

        static可以用来修饰类的成员方法、类的成员变量,另外可以编写static代码块来优化程序性能。

1)static方法

static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。

但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。举个简单的例子:

Java面向对象

在上面的代码中,由于print2方法是独立于对象存在的,可以直接用过类名调用。假如说可以在静态方法中访问非静态方法/变量的话,那么如果在main方法中有下面一条语句:

MyObject.print2();

此时对象都没有,str2根本就不存在,所以就会产生矛盾了。同样对于方法也是一样,由于你无法预知在print1方法中是否访问了非静态成员变量,所以也禁止在静态成员方法中访问非静态成员方法。

而对于非静态成员方法,它访问静态成员方法/变量显然是毫无限制的。

因此,如果说想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。我们最常见的static方法就是main方法,至于为什么main方法必须是static的,现在就很清楚了。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。

 2)static变量

static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

static成员变量的初始化顺序按照定义的顺序进行初始化。

 3)static代码块

static关键字还有一个比较关键的作用就是用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。

为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。下面看个例子:

class Person{
    private Date birthDate;
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        Date startDate = Date.valueOf("1946");
        Date endDate = Date.valueOf("1964");
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

isBornBoomer是用来这个人是否是1946-1964年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好:

class Person{
    private Date birthDate;
    private static Date startDate,endDate;
    static{
        startDate = Date.valueOf("1946");
        endDate = Date.valueOf("1964");
    }
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

二.static关键字的误区

1.static关键字会改变类中成员的访问权限吗?

有些初学的朋友会将java中的static与C/C++中的static关键字的功能混淆了。在这里只需要记住一点:与C/C++中的static不同,Java中的static关键字不会影响到变量或者方法的作用域。在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字。看下面的例子就明白了:

Java面向对象

提示错误"Person.age 不可视",这说明static关键字并不会改变变量和方法的访问权限。

2.能通过this访问静态成员变量吗?

虽然对于静态方法来说没有this,那么在非静态方法中能够通过this访问静态成员变量吗?先看下面的一个例子,这段代码输出的结果是什么?

public class Main {  
    static int value = 33;
 
    public static void main(String[] args) throws Exception{
        new Main().printValue();
    }
 
    private void printValue(){
        int value = 3;
        System.out.println(this.value);
    }
}

33

这里面主要考察对this和static的理解。this代表什么?this代表当前对象,那么通过new Main()来调用printValue的话,当前对象就是通过new Main()生成的对象。而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是33。在printValue方法内部的value是局部变量,根本不可能与this关联,所以输出结果是33。在这里永远要记住一点:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。

3.static能作用于局部变量么?

在C/C++中static是可以作用域局部变量的,但是在Java中切记:static是不允许用来修饰局部变量。不要问为什么,这是Java语法的规定。

三.常见的笔试面试题

1.下面这段代码的输出结果是什么?

public class Test extends Base{
 
    static{
        System.out.println("test static");
    }
     
    public Test(){
        System.out.println("test constructor");
    }
     
    public static void main(String[] args) {
        new Test();
    }
}
 
class Base{
     
    static{
        System.out.println("base static");
    }
     
    public Base(){
        System.out.println("base constructor");
    }
}
base static
test static
base constructor
test constructor

至于为什么是这个结果,我们先不讨论,先来想一下这段代码具体的执行过程,在执行开始,先要寻找到main方法,因为main方法是程序的入口,但是在执行main方法之前,必须先加载Test类,而在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类,在加载Base类的时候,发现有static块,便执行了static块。在Base类加载完成之后,便继续加载Test类,然后发现Test类中也有static块,便执行static块。在加载完所需的类之后,便开始执行main方法。在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。

2.这段代码的输出结果是什么?

public class Test {
    Person person = new Person("Test");
    static{
        System.out.println("test static");
    }
     
    public Test() {
        System.out.println("test constructor");
    }
     
    public static void main(String[] args) {
        new MyClass();
    }
}
 
class Person{
    static{
        System.out.println("person static");
    }
    public Person(String str) {
        System.out.println("person "+str);
    }
}
 
 
class MyClass extends Test {
    Person person = new Person("MyClass");
    static{
        System.out.println("myclass static");
    }
     
    public MyClass() {
        System.out.println("myclass constructor");
    }
}
test static
myclass static
person static
person Test
test constructor
person MyClass
myclass constructor

类似地,我们还是来想一下这段代码的具体执行过程。首先加载Test类,因此会执行Test类中的static块。接着执行new MyClass(),而MyClass类还没有被加载,因此需要加载MyClass类。在加载MyClass类的时候,发现MyClass类继承自Test类,但是由于Test类已经被加载了,所以只需要加载MyClass类,那么就会执行MyClass类的中的static块。在加载完之后,就通过构造器来生成对象。而在生成对象的时候,必须先初始化父类的成员变量,因此会执行Test中的Person person = new Person(),而Person类还没有被加载过,因此会先加载Person类并执行Person类中的static块,接着执行父类的构造器,完成了父类的初始化,然后就来初始化自身了,因此会接着执行MyClass中的Person person = new Person(),最后执行MyClass的构造器。

3.这段代码的输出结果是什么?

public class Test {
     
    static{
        System.out.println("test static 1");
    }
    public static void main(String[] args) {
         
    }
     
    static{
        System.out.println("test static 2");
    }
}
test static 1
test static 2

虽然在main方法中没有任何语句,但是还是会输出,原因上面已经讲述过了。另外,static块可以出现类中的任何地方(只要不是方法内部,记住,任何方法内部都不行),并且执行是按照static块的顺序执行的。

2、final

使用final关键字做标识有“最终的”含义。

        1. final 修饰类,则该类不允许被继承。

        2. final 修饰方法,则该方法不允许被覆盖(重写)

        3. final 修饰属性,则该类的该属性不会进行隐式的初始化,所以 该final 属性的初始化属性必须有值,或在构造方法中赋值(但只能选其一,且必须选其一,因为没有默认值!),且初始化之后就不能改了,只能赋值一次

        4. final 修饰变量,则该变量的值只能赋一次值,在声明变量的时候才能赋值,即变为常量

3、this

1.this关键字代表当前对象

        this.属性 操作当前对象的属性

        this.方法 调用当前对象的方法。

2.封装对象的属性的时候,经常会使用this关键字。

3.当getter和setter函数参数名和成员函数名重合的时候,可以使用this区别。如: 

Java面向对象

 4、super

在对象的内部使用,可以代表父类对象。

        1、访问父类的属性:super.age

        2、访问父类的方法:super.eat()

super的应用:

        首先我们知道子类的构造的过程当中必须调用父类的构造方法。其实这个过程已经隐式地使用了我们的super关键字。

        这是因为如果子类的构造方法中没有显示调用父类的构造方法,则系统默认调用父类无参的构造方法。

        那么如果自己用super关键字在子类里调用父类的构造方法,则必须在子类的构造方法中的第一行

        要注意的是:如果子类构造方法中既没有显示调用父类的构造方法,而父类没有无参的构造方法,则编译出错。

(补充说明,虽然没有显示声明父类的无参的构造方法,系统会自动默认生成一个无参构造方法,但是,如果你声明了一个有参的构造方法,而没有声明无参的构造方法,这时系统不会动默认生成一个无参构造方法,此时称为父类有没有无参的构造方法。)

四、初始化

1、初始化父类再初始化子类

2、先执行初始化对象中属性,再执行构造方法中的初始化。

基于上面两点,我们就知道实例化一个子类,java程序的执行顺序是:

父类对象属性初始化---->父类对象构造方法---->子类对象属性初始化--->子类对象构造方法   

下面有个形象的图:

Java面向对象

五、属性和方法

1、构造方法

概念:构造方法是一个特殊的方法,这个特殊方法用于创建实例时执行初始化操作;

package com.java1234.chap03.sec04;

/**
 * 定义人类
 * @author user
 *
 */
public class People {

    // 定义属性
    private String name; // 实例化对象时,默认值是null
    private int age; // 实例化对象时,默认值是0

    /**
     * 默认构造方法
     */
    People(){
        System.out.println("默认构造方法!");
    }



    public void say(){
        System.out.println("我叫:"+name+",我今年:"+age);
    }

    public static void main(String[] args) {
        People people=new People();
        people.say();
    }
}
运行输出:

默认构造方法!

我叫:null,我今年:0

这里我们发现: 实例化对象的时候,String类型的默认值是null,int基本类型的默认值是0 ;

People(){} 构造方法

特点 1,没有返回值类型,区别于其他一般方法;

        2,方法名和类名一样;

我们现在可以写一个有参数的构造方法,用来初始化对象属性;

package com.java1234.chap03.sec04;

/**
 * 定义人类
 * @author user
 *
 */
public class People {

    // 定义属性
    private String name; // 实例化对象时,默认值是null
    private int age; // 实例化对象时,默认值是0

    /**
     * 默认构造方法
     */
    People(){
        System.out.println("默认构造方法!");
    }

    /**
     * 有参数的构造方法
     * @param name2
     * @param age2
     */
    People(String name2,int age2){
        System.out.println("调用的是有参数的构造方法");
        name=name2;
        age=age2;
    }

    public void say(){
        System.out.println("我叫:"+name+",我今年:"+age);
    }

    public static void main(String[] args) {
        // People people=new People();
        People people=new People("张三",20);
        people.say();
    }
}
运行结果

调用的是有参数的构造方法

我叫:张三,我今年:20

 这里我们定义了一个有参数的构造方法,参数有name2 age2,当调用构造方法的时候,用于赋值给name和age属性;

注意点:

1,假如没有构造方法,系统会自动生成一个默认的无参构造方法;

上代码,我们去掉刚才定义的People(){} 

package com.java1234.chap03.sec04;

/**
 * 定义人类
 * @author user
 *
 */
public class People {

    // 定义属性
    private String name; // 实例化对象时,默认值是null
    private int age; // 实例化对象时,默认值是0

    /**
     * 默认构造方法
     */
    /*People(){
        System.out.println("默认构造方法!");
    }*/

    /**
     * 有参数的构造方法
     * @param name2
     * @param age2
     */
    /*People(String name2,int age2){
        System.out.println("调用的是有参数的构造方法");
        name=name2;
        age=age2;
    }*/

    public void say(){
        System.out.println("我叫:"+name+",我今年:"+age);
    }

    public static void main(String[] args) {
        People people=new People();
        //People people=new People("张三",20);
        people.say();
    }
}

我们把两个构造方法注释了,运行的话,依然没问题。

2,假如有构造方法,系统不会自动生成构造方法;

假如我们单独去掉无参数的构造方法,上代码:

package com.java1234.chap03.sec04;

/**
 * 定义人类
 * @author user
 *
 */
public class People {

    // 定义属性
    private String name; // 实例化对象时,默认值是null
    private int age; // 实例化对象时,默认值是0


    /**
     * 有参数的构造方法
     * @param name2
     * @param age2
     */
    People(String name2,int age2){
        System.out.println("调用的是有参数的构造方法");
        name=name2;
        age=age2;
    }

    public void say(){
        System.out.println("我叫:"+name+",我今年:"+age);
    }

    public static void main(String[] args) {
        People people=new People();
        //People people=new People("张三",20);
        people.say();
    }
}

 我们会发现编译报错了:

Java面向对象

The constructor People() is undefined

说找不到People()构造方法 

2、重载和重写

子类如果对继承的父类的方法不满意(不适合),可以自己编写继承的方法,这种方式就称为方法的重写。当调用方法时会优先调用子类的方法。

重写要注意:

        a、返回值类型

        b、方法名

        c、参数类型及个数

都要与父类继承的方法相同,才叫方法的重写。

重载和重写的区别:

        方法重载:在同一个类中处理不同数据的多个相同方法名的多态手段。

        方法重写:相对继承而言,子类中对父类已经存在的方法进行区别化的修改。

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

最常用的地方就是构造器的重载。

重载要注意:

        a、返回类型可以相同也可以不同

        b、方法名字相同

        c、参数不同

六、向上转型、向下转型

引用类型转换:

1. 向上类型转换(隐式/自动类型转换),是小类型转换到大类型。

当父类的引用可以指向子类的对象时,就是向上类型转换。如:

Java面向对象

 2. 向下类型转换(强制类型转换),是大类型转换到小类型(有风险,可能出现数据溢出)

将上述代码再加上一行,我们再次将父类转换为子类引用,那么会出现错误,编译器不允许我们直接这么做虽然我们知道这个父类引用指向的就是子类对象,但是编译器认为这种转换是存在风险的如:

Java面向对象

那么我们该怎么解决这个问题呢,我们可以在animal前加上(Dog)来强制类型转换。如:

Java面向对象

但是如果父类引用没有指向该子类的对象,则不能向下类型转换,虽然编译器不会报错,但是运行的时候程序会出错,如:

Java面向对象

其实这就是上面所说的子类的引用指向父类的对象,而强制转换类型也不能转换!!

还有一种情况是父类的引用指向其他子类的对象,则不能通过强制转为该子类的对象。如:

Java面向对象

这是因为我们在编译的时候进行了强制类型转换,编译时的类型是我们强制转换的类型,所以编译器不会报错,而当我们运行的时候,程序给animal开辟的是Dog类型的内存空间,这与Cat类型内存空间不匹配,所以无法正常转换。这两种情况出错的本质是一样的,所以我们在使用强制类型转换的时候要特别注意这两种错误!!下面有个更安全的方式来实现向下类型转换

3. instanceof运算符,来解决引用对象的类型,避免类型转换的安全性问题。

instanceof是Java的一个二元操作符,和==,>,<是同一类东东。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。

我们来使用instanceof运算符来规避上面的错误,代码修改如下:

Java面向对象

利用if语句和instanceof运算符来判断两个对象的类型是否一致。 

补充说明:在比较一个对象是否和另一个对象属于同一个类实例的时候,我们通常可以采用instanceof和getClass两种方法通过两者是否相等来判断,但是两者在判断上面是有差别的。Instanceof进行类型检查规则是:你属于该类吗?或者你属于该类的派生类吗?而通过getClass获得类型信息采用==来进行检查是否相等的操作是严格的判断,不会存在继承方面的考虑

总结:在写程序的时候,如果要进行类型转换,我们最好使用instanceof运算符来判断它左边的对象是否是它右边的类的实例,再进行强制转换。

七、内部类

我们知道内部类可分为以下几种:

  • 成员内部类
  • 静态内部类
  • 方法内部类
  • 匿名内部类

这里我们先将以这个分类来详细了解各个内部类的情况。然后给内部类作出总结。

1、成员内部类

内部类中最常见的就是成员内部类,也称为普通内部类。我们来看如下代码:

Java面向对象

运行结果为:Java面向对象 

从上面的代码中我们可以看到,成员内部类的使用方法

  1、 Inner 类定义在 Outer 类的内部相当于 Outer 类的一个成员变量的位置,Inner 类可以使用任意访问控制符,如 public 、 protected 、 private 等

  2、 Inner 类中定义的 test() 方法可以直接访问 Outer 类中的数据,而不受访问控制符的影响,如直接访问 Outer 类中的私有属性a

  3、 定义了成员内部类后,必须使用外部类对象来创建内部类对象,而不能直接去 new 一个内部类对象,即:内部类 对象名 = 外部类对象.new 内部类( );

  4、 编译上面的程序后,会发现产生了两个 .class 文件

Java面向对象

其中,第二个是外部类的 .class 文件,第一个是内部类的 .class 文件,即成员内部类的 .class 文件总是这样:外部类名$内部类名.class

另外,友情提示哦:

1、 外部类是不能直接使用内部类的成员和方法滴。如:

Java面向对象

那么外部类如何使用内部类的成员和方法呢??

答:可先创建内部类的对象,然后通过内部类的对象来访问其成员变量和方法。

Java 编译器在创建内部类对象时,隐式的把其外部类对象的引用也传了进去并一直保存着。这样就使得内部类对象始终可以访问其外部类对象,同时这也是为什么在外部类作用范围之外向要创建内部类对象必须先创建其外部类对象的原因。

2、 如果外部类和内部类具有相同的成员变量或方法,内部类默认访问自己的成员变量或方法,如果要访问外部类的成员变量,可以使用 this 关键字。如:

Java面向对象

运行结果:Java面向对象 

2、静态内部类 

静态内部类是 static 修饰的内部类,这种内部类的特点是:

        1、 静态内部类不能直接访问外部类的非静态成员,但可以通过 new 外部类().成员 的方式访问。

        2、 如果外部类的静态成员与内部类的成员名称相同,可通过“类名.静态成员”访问外部类的静态成员;如果外部类的静态成员与内部类的成员名称不相同,则可通过“成员名”直接调用外部类的静态成员。

        3、 创建静态内部类的对象时,不需要外部类的对象,可以直接创建 内部类 对象名= new 内部类();

Java面向对象

运行结果 : Java面向对象 

3、方法内部类 

 方法内部类就是内部类定义在外部类的方法中,方法内部类只在该方法的内部可见,即只在该方法内可以使用

Java面向对象

一定要注意哦:由于方法内部类不能在外部类的方法以外的地方使用,因此方法内部类不能使用访问控制符和 static 修饰符。

4、匿名内部类

匿名类是不能有名称的类,所以没办法引用他们。必须在创建时,作为new语句的一部分来声明他们。

但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口。
这就要采用另一种形式 的new语句,如下所示:

new <类或接口> <类的主体>

这种形式的new语句声明一个新的匿名类,他对一个给定的类进行扩展,或实现一个给定的接口。他还创建那个类的一个新实例,并把他作为语句的结果而返回。要扩展的类和要实现的接口是 new语句的操作数,后跟匿名类的主体

注意匿名类的声明是在编译时进行的,实例化在运行时进行。这意味着 for循环中的一个new语句会创建相同匿名类的几个实例,而不是创建几个不同匿名类的一个实例。

从技术上说,匿名类可被视为非静态的内部类,所以他们具备和方法内部声明的非静态内部类相同的权限和限制。

 假如要执行的任务需要一个对象,但却不值得创建全新的对象(原因可能 是所需的类过于简单,或是由于他只在一个方法内部使用),匿名类就显得很有用。匿名类尤其适合在Swing应用程式中快速创建事件处理程式。以下是一个匿名内部类的实例:

1、匿名内部类的基本实现:

Java面向对象

运行结果:Java面向对象 

可以看到,我们直接将抽象类Person中的方法在大括号中实现了,这样便可以省略一个类的书写,并且,匿名内部类还能用于接口上。 

2、在接口上使用匿名内部类:

Java面向对象

运行结果:Java面向对象 

由上面的例子可以看出,只要一个类是抽象的或是一个接口,那么其子类中的方法都可以使用匿名内部类来实现。

在使用匿名内部类的过程中,我们需要注意如下几点:

1、使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。

2、匿名内部类中是不能定义构造函数的。

3、匿名内部类中不能存在任何的静态成员变量和静态方法。

4、匿名内部类为局部内部类(即方法内部类),所以局部内部类的所有限制同样对匿名内部类生效。

5、匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。

匿名内部类重点难点 

1. 如果是在一个方法的匿名内部类,可以利用这个方法传进你想要的参数,不过记住,这些参数必须被声明为 final 。

使用的形参为何要为final??

我们给匿名内部类传递参数的时候,若该形参在内部类中需要被使用,那么该形参必须要为final。也就是说:当所在的方法的形参需要被内部类里面使用时,该形参必须为final。

首先我们知道在内部类编译成功后,它会产生一个class文件,该class文件与外部类并不是同一class文件,仅仅只保留对外部类的引用。当外部类传入的参数需要被内部类调用时,从java程序的角度来看是直接被调用:

public class OuterClass {  
        public void display(final String name,String age){  
            class InnerClass{  
                void display(){  
                    System.out.println(name);  
                }  
            }  
        }  
    }

从上面代码中看好像name参数应该是被内部类直接调用?其实不然,在java编译之后实际的操作如下:

public class OuterClass$InnerClass {  
        public InnerClass(String name,String age){  
            this.InnerClass$name = name;  
            this.InnerClass$age = age;  
        }  
          
          
        public void display(){  
            System.out.println(this.InnerClass$name + "----" + this.InnerClass$age );  
        }  
    }

所以从上面代码来看,内部类并不是直接调用方法传递的参数,而是利用自身的构造器对传入的参数进行备份,自己内部方法调用的实际上时自己的属性而不是外部方法传递进来的参数。

直到这里还没有解释为什么是final。在内部类中的属性和外部方法的参数两者从外表上看是同一个东西,但实际上却不是,所以他们两者是可以任意变化的,也就是说在内部类中我对属性的改变并不会影响到外部的形参,而然这从程序员的角度来看这是不可行的,毕竟站在程序的角度来看这两个根本就是同一个,如果内部类该变了,而外部方法的形参却没有改变这是难以理解和不可接受的,所以为了保持参数的一致性,就规定使用final来避免形参的不改变。

简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。

故如果定义了一个匿名内部类,并且希望它使用一个其外部定义的参数,那么编译器会要求该参数引用是final的。

2. 匿名内部类中使用初始化代码块

我们一般都是利用构造器来完成某个实例的初始化工作的,但是匿名内部类是没有构造器的!那怎么来初始化匿名内部类呢?使用构造代码块!利用构造代码块能够达到为匿名内部类创建一个构造器的效果。

public class OutClass {  
        public InnerClass getInnerClass(final int age,final String name){  
            return new InnerClass() {  
                int age_ ;  
                String name_;  
                //构造代码块完成初始化工作  
                {  
                    if(0 < age && age < 200){  
                        age_ = age;  
                        name_ = name;  
                    }  
                }  
                public String getName() {  
                    return name_;  
                }  
                  
                public int getAge() {  
                    return age_;  
                }  
            };  
        }  
          
        public static void main(String[] args) {  
            OutClass out = new OutClass();  
              
            InnerClass inner_1 = out.getInnerClass(201, "chenssy");  
            System.out.println(inner_1.getName());  
              
            InnerClass inner_2 = out.getInnerClass(23, "chenssy");  
            System.out.println(inner_2.getName());  
        }  
    }

5、内部类总结

学习了上面四种类型的内部类,我们知道了如何使用各个内部类,那么为什么要使用内部类呢??

先举一个简单的例子,如果你想实现一个接口,但是这个接口中的一个方法和你构想的这个类中的一个方法的名称,参数相同,你应该怎么办?这时候,你可以建一个内部类实现这个接口。由于内部类对外部类的所有内容都是可访问的,所以这样做可以完成所有你直接实现这个接口的功能。
  不过你可能要质疑,更改一下方法的不就行了吗?的确,以此作为设计内部类的理由,实在没有说服力。

真正的原因是这样的,java中的内部类和接口加在一起,可以的解决常被C++程序员抱怨java中存在的一个问题——没有多继承。实际上,C++的多继承设计起来很复杂,而java通过内部类加上接口,可以很好的实现多继承的效果。

内部类:一个内部类的定义是定义在另一个内部的类。

原因是:
  1.一个内部类的对象能够访问创建它的对象的实现,包括私有数据。
  2.对于同一个包中的其他类来说,内部类能够隐藏起来。
  3.匿名内部类可以很方便的定义回调。
  4.使用内部类可以非常方便的编写事件驱动程序。

内部类可以让你更优雅地设计你的程序结构。下面从以下几个方面来介绍:

首先看这个例子:

public interface Contents {
     int value();
    }

    public interface Destination {
     String readLabel();
    }
public class Goods {
      private valueRate=2;

     private class Content implements Contents {
       private int i = 11 * valueRate;
      public int value() {
       return i;
      }
     }

     protected class GDestination implements Destination {
      private String label;
      private GDestination(String whereTo) {
       label = whereTo;
      }
      public String readLabel() {
       return label;
      }
     }

     public Destination dest(String s) {
      return new GDestination(s);
     }

      public Contents cont() {
      return new Content();
     }
    }

在这个例子里类 Content 和 GDestination 被定义在了类 Goods 内部,并且分别有着 protected 和 private 修饰符来控制访问级别。Content 代表着 Goods 的内容,而 GDestination 代表着 Goods 的目的地。它们分别实现了两个接口Content和Destination。在后面的main方法里,直接用 Contents c 和 Destination d进行操作,你甚至连这两个内部类的名字都没有看见!这样,内部类的第一个好处就体现出来了——隐藏你不想让别人知道的操作,也即封装性。

非静态内部类对象有着指向其外部类对象的引用

public class Goods {
      private valueRate=2;

     private class Content implements Contents {
       private int i = 11 * valueRate;
      public int value() {
       return i;
      }
     }

     protected class GDestination implements Destination {
      private String label;
      private GDestination(String whereTo) {
       label = whereTo;
      }
      public String readLabel() {
       return label;
      }
     }

     public Destination dest(String s) {
      return new GDestination(s);
     }

      public Contents cont() {
      return new Content();
     }
    }

在这里我们给 Goods 类增加了一个 private 成员变量 valueRate,意义是货物的价值系数,在内部类 Content 的方法 value() 计算价值时把它乘上。我们发现,value() 可以访问 valueRate,这也是内部类的第二个好处——一个内部类对象可以访问创建它的外部类对象的内容,甚至包括私有变量!这是一个非常有用的特性,为我们在设计时提供了更多的思路和捷径。要想实现这个功能,内部类对象就必须有指向外部类对象的引用。Java 编译器在创建内部类对象时,隐式的把其外部类对象的引用也传了进去并一直保存着。这样就使得内部类对象始终可以访问其外部类对象,同时这也是为什么在外部类作用范围之外向要创建内部类对象必须先创建其外部类对象的原因。

版权声明:程序员胖胖胖虎阿 发表于 2022年9月8日 上午2:08。
转载请注明:Java面向对象 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...