Java面向对象上(四)

下一篇:面向对象下(五)

1.面向对象与面向过程

1.1面向过程的结构化程序设计

        面向过程是一种以过程为中心的编程思想,面向过程就是分析出问题的解决步骤,然后用函数将这些步骤一步一步实现。拿我们早晨去跑步来说,我们首先是起床,然后是洗漱,再一个穿衣,最后出门。面向过程就是这样一步步地去执行——过程。
        结构化程序设计设计方法主张按功能来分析需求,主要原则概括为自顶而下,逐步求精,模块化等。结构化程序设计首先是结构化分析(Structrued Analysis ,即SA),然后是结构化设计(Structrued Design,即SD),最后是结构化编程(Structrued Program,即SP)。这三种方式可以较好保证软件系统的开发进度和质量。
        面向过程的结构化程序设计分为3种结构:顺序结构、选择结构、循环结构。
        原则:
         1. 自顶而上:从一个问题的全局下手,把复杂的问题分解成许多个子问题,然后再把子问题细分直到问题解决为止。
         2. 逐步求精。
         3. 模块化:解决一个问题是自顶而下的把软件系统分解成一个个较小的、相互独立的又是相互关联的模块的过程。
        结构化程序设计中最小的程序单元是函数,每个函数完成一个功能。
        结构化程序设计的局限性:
         1.设计不够直观,与人类思维不一致。设计,需要将客观世界模型分解成一个个功能。
         2. 适应性差,扩展性不强。由于采用自顶而下的结构化设计,当用户需求改变时,需要改变自顶而下改变模块结构,维护成本很大。

1.1面向对象

        面向对象按照人们认识客观世界的思维方式,采用基于对象(实体)的概念建立模型,模拟客观世界分析,设计,处理的方式进行编程。还是拿早上去跑步来说,与面向过程不同的是,我们无需一步步执行,我们可以不按上面流程来,我们可以先去跑步,回来再洗漱。
        面向对象的最小化结构单元是类。封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)是面向对象的三大特征。
        优点:将复杂化变得简单化,更符合人类认知的思想,可维护性和扩展性都大大提高。





2.类和对象

        面向对象有两个重要概念:类(Class)和对象(Object,也称实例)。Java是面向对象的设计语言,类和对象是面向对象的设计核心。对象的抽象是类,类的具体化是对象。


2.1类的定义

语法

修饰符 class 类名{
	构造器
	属性
	方法
}
例如:
public class Person{
	//属性
	private String name;
	private int age;
	//构造器
	public Person(){
	}
	public Person(String name,int age){
		this.name=name;
		this,age=age;
	}
	//方法
	public String getName(){
		return this.name;
	}
}

        对于一个类而言,最常见的成员就是:构造器、属性、方法。类成员之间的定义顺序没有要求。

构造器不是没有返回值吗?什么不能用void修饰呢?

:简单地说就是Java语法的规定。但是实际上构造器是有返回值的,当我们使用new关键字是用来调用构造器的,调用之后会返回该类的一个实例。所以说构造器是有返回类型的,他返回的是这个类的一个实例,但是不需要我们显示的指定返回类型,我们也不能使用return返回这个类型。

属性

        属性用于定义该类或者该类的实例所包含的数据,格式:修饰符 属性类型 属性名 (=默认值)。属性是一种比较符合汉语习惯的说法,在Java的官方文档中,属性称为"Field",也有地方将属性翻译为字段。
        属性修饰符:属性修饰符可以省略,也可以是publicprotectedprivatestaticfinal。其中publicprotectedprivate三选一,只能出现一个。staticfinal可以同时出现。
        属性类型:属性类型可以是Java语言允许的任何类型。
        属性名:一般建议属性名是一个或多个有意义的单词,首字母小写,使用驼峰命名法。

方法

        方法的返回类型:方法的返回类型也是Java允许的任何数据类型,包括基本类型和引用类型,使用return语句返回需要返回类型的一个变量值,或者是表达式。void表示无返回类型,不需要返回。
        方法修饰符:属性修饰符可以省略,也可以是publicprotectedprivatestaticfinalabstract。其中publicprotectedprivate用的最多。finalabstract只能出现其中的一个。
        方法名:方法名命名建议与属性名一样,由一个或多个有意义的单词组成,首字母小写。
        方法形参列表:形参列表用于定义该方法可以接受的参数,可以没有也可以有,多个形参直接使用逗号(,)隔开,形参类型与形参名用空格隔开。。一旦设置了形参,在调用就要传入对应参数——谁调用谁负责传参。

构造器

        构造器是一个特殊的方法,这个方法用于创建实例。Java语言里构造器是创建对象的核心(即使使用工厂模式,反射等方式创建对象,其实质依然是依赖于构造器)。因此,Java类必须包含一个或者多个构造器。
        构造器不能定义返回值类型,也不能使用void定义构造器没有返回值,如果设定了返回值,或者说设为void,系统会默认将这个作为一个方法处理。我们一般使用new调用构造器。构造器名必须与类名相同。若在一个类中没有显式地定义一个构造器,系统会有一个默认的无参构造器,一旦定义构造器,这个默认的无参构造器也不存在。一般情况,定义了有参构造器之后,建议定义一个无参构造器,这也是构造器的重载。
        构造器修饰符:修饰符可以省略,也可以是publicprotectedprivate三个中的一个。
        构造器返回类型:虽然构造器没有显式地将返回类型写出来,也没有显式地返回类型。但是系统默认构造器的返回类型为当前类的类型,默认返回当前对象。构造器名必须与类名相同
        构造器形参列表:定义格式与方法形参列表相同。

构造器是创建Java对象的途径,是不是说构造器完全负责创建Java对象?

不是!构造器是创建Java对象的重要途径,通过new关键字可以调用构造器来创建对象,但这个对象并不是全由构造器穿件的。实际上,当程序员调用构造器的时候,系统会光为该对象分配内存空间,并为这个对象执行默认初始化,这个对象已经产生了——这些都是在构造器执行之前完成了。也就是说,当系统调用构造器之前,系统已经创建了一个对象,只是这个对象不能被外部程序访问,只能在该构造器中通过this引用。当构造器执行结束后,这个对象作为构造器的返回值返回,通常还会赋给另一个引用对象的变量,从而让外部程序可以访问该对象。

        有时候我们可以使用protected修饰构造器,主要用于被子类调用;当我们不希望其他类创建这个类的实例时,我们可以使用private修饰构造器。

构造器的重载
同一个类可以有多个构造器,多个构造器之后形参列表不同,这就叫做构造器的重载。构造器重载与方法重载类似。




2.2对象的产生和使用

        创建构造器的根本途径是构造器,通过new关键字创建这个类的实例。例如:

Person p=new Person();

引用变量指向:
Person对象
        栈内存理的引用变量并没有存储对象里的属性数据,对象的属性数据实际存放在堆内存中,只能通过该对象的引用操作该对象。

Person p2=p;

        p实际上就是存储了一个地址,将p的变量的值赋给p2,这样p和p2实际上是指向同一个变量,不管是访问p还是p2他们实际上是同一个Person对象,将会返回相同的结果。




2.3对象的this引用

        Java提供了一个关键字this,this关键字是一个对象的默认引用,这个this总是指向调用该方法的对象,this出现的情况:

  1. 构造器中引用该构造器的执行初始化的对象。
  2. 在方法中引用调用该方法的对象。

        this的最大作用就是让类中一个方法,访问该类的另一个方法或者属性。实际上,大部分时候我们在使用这个类的其他方法不使用this效果也是一样的。

  • static:翻译过来就是静态的意思,使用static修饰的方法也叫静态方法,static修饰的属性也称静态属性或者说静态成员变量,没有用static修饰的则为非静态。

        1. 对于用static修饰的方法调用该类的其他方法或属性的方式有两种,一是调用该类的static方法或者属性,二是通过该类的一个对象(实例)调用方法或者属性。
        2. 对于static修饰的方法而言,可以直接调用该方法,如果在static修饰的方法中使用this这个关键字,则这个关键字无法指向合适的对象,所有static不能使用this引用。




2.4方法详解

2.4.1方法的所属性

        不管是从语法还是从功能上来看,都不难发现方法和函数有很多的相似之处,实际上方法就是由传统的函数发展而来的,但方法和传统的函数有着很多的不同:在结构化的语言里,函数就像是一个个公民,整个软件就像是一个国家,一个国家有着许多的公民,一个软件也是由许多的功能组成;在面向对象的编程中,类相当于公民,整个软件是由一个个类组成,在Java语言里,方法不能脱离类单独存在,方法必须在类里面。
        如果这个方法使用了static修饰,那么这个方法属于这个类;否者这个方法属于这个类的对象。Java的所属性表现为:

  1. 方法不能独立定义,必须定义在类里面
  2. 从逻辑意义上来看,方法要么属于一个类,要么属于一个对象。
  3. 永远不能独立执行方法,执行方法必须使用类或对象来作为调用者。
2.4.2方法的参数传递

        Java的实参值是如何传入方法的你?这是由Java方法的参数传递机制控制的,Java里方法的参数参数传递方式只有一种:值传递。所谓的值传递,就是将实际参数的副本(复制品)传递到方法内,而参数本身不受任何影响。

        Java里参数的传递类似于我们复制了一份文件,当我们修改这个复制文件时,不会对原来的文件造成影响。类似的,这个方法传入的参数也是一个复制品,不会对原来的造成影响。

   public static void main(String[] args) {
        int a=1;
        int b=5;
        swap(a,b);
        System.out.println("main方法里面:a="+a+"  b="+b);//main方法里面:a=1  b=5
    }

    public static void swap(int a,int b){
        int temp=a;
        a=b;
        b=temp;
        System.out.println("swap方法里面:a="+a+"  b="+b);//swap方法里面:a=5  b=1
    }

执行结果:
执行结果
        我们来逐步分析上面这段代码,首先定义a变量并初始化为1,然后定义b变量并初始化为5,再调用swap(交换方法,实现两个数交换),将a,b分别传进去。

  1. 分别定义a、b变量并分别初始化
  2. 在这里插入图片描述
  3. 调用swap方法
    在这里插入图片描述
  4. swap方法中交换
    在这里插入图片描述
    我们可以看出在swap中交换,并没有对main方法的参数造成任何影响。
2.4.3形参长度可变的方法

        在JDK1.5以后,Java允许定义可变的形参的方法。使用可变形参是在定义方法的时候在所在参数类型后面添加三个点(...),这样就是表明形参可以接受多个参数。

public static void test(String... books){
	for(String book:books){
		System.out.println(book);
	}
}
public static void main(String[] args){
	//调用test方法
	test("十万个为什么","格林童话","安徒生童话");
}

运行结果

十万个为什么
格林童话
安徒生童话

        上面使用的是可变的形参方式。当然我们可以使用数组的方式。

public static void test(String[] books){
	for(String book:books){
		System.out.println(book);
	}
}

        我们可以看到使用可变形参和使用数组得的结果是一样的,使用可变形参代码看起来更加简洁了。值得说明的是,使用数组的方式,这个数组形参可以放在参数的任意位置;但是使用可变形参,这个可变的形参只能放在参数的最后。

       注意:使用可变长度的形参时,这个可变长度的形参只能放在形参的最后。并且一个方法中指允许一个这样·可变长度的形参。调用带有可变长度的形参的方法时,这个可变长度的形参可以是有很多个参数,也可以传入一个数组。

2.4.4递归方法

       递归:递归是指一个方法调用自身。
       方法的递归隐式包含了一种循环,会重复执行某段代码,因此递归一定要有一个控制结束的条件,否则将会无线循环下去。

public static int recursion(int n,int result){
	if(n==1){
		return result+n;
	}
	result+=n;
	return recursion(n--,result);
}
public static void main(String[] args){
	//计算1+2+3+...+100的值
	System.out.println(recursion(100,0));
}

       我们从上面这段递归代码中,可以看出递归的出口是当n为1时。当不为1时,将会自己调用自己。

2.4.5方法重载

       重载:方法重载是指在同一个类中定义多个方法名相同的方法,这些方法的形参不同。
       方法的三要素

  1. 方法的所属者,既可以是类,也可以是对象。
  2. 方法名,方法的标识
  3. 形参列表,调用方法时,系统会根据传入的参数自动匹配。

       而方法重载是两同,一不同。即方法的所属者和方法名相同,形参列表不相同。方法的重载与返回类型,修饰符等没有关系。

       注意:特别值得注意的一点是,方法的返回类型不同不是重载,我们也不能用返回类型不同来区分重载。Java在调用的时候可以忽略方法的返回值,如果两个方法所属同一个类,方法名和形参列表也相同,而返回类型不同,当我们调用这个方法时,能区分调用的是哪个方法吗?显然是不能的。

public static void test(String... books){
	for(String book:books){
		System.out.println(book);
	}
	System.out.println("形参可变");
}
public static void test(String msg){
	System.out.println(msg);
	System.out.println("一个参数");
}
public static void main(String[] args){
	//调用test方法
	test();
	test("aa");
	test("十万个为什么","格林童话","安徒生童话");
}

       运行上面程序,我们可以发现当调用test();test("十万个为什么","格林童话","安徒生童话");这两个都是调用的可变参数方法;test("aa");则调用的是一个参数的方法。大部分时间我们,我们不推荐可变长度形参的方法重载,这样降低代码的可读性。




2.5成员变量和局部变量

       根据变量定义位置的不同可以把变量分为:成员变量和局部变量。不管是成员变量还是局部变量他们的命名规则都是相同的,这两种变量运行机制存在很大的差异。
变量分类

2.5.1成员变量

       成员变量:成员变量是定义在类范围内的变量,也就是我们前面说到的属性。成员变量分为两种:一种是实例属性,就是不使用static修饰的成员变量,当创建这个类的属性时,才会为这个属性分配内存空间;还有一种数类属性,使用static修饰,它是属于这个类的,这个类存在会为这个属性分配空间,不管创建多少个这个类的实例都只有一个。在访问权限允许的情况下,这个属性既可以通过类来调用,也可以通过这个类的实例调用,但不管用哪种方式实际上操作的都是同一个。

  • 一个类在使用之前都经过类加载,类验证,类准备,类解析,类初始化等这个几个阶段。
  • 类属性在这个类的准备阶段开始存在,直到系统完全将这个类销毁,类属性的作用域与这个类的生存范围相同。

       类属性与这个类共存亡,而实例属性与这个实例共存亡。

Person类

public class Person {
    //类属性,眼睛的数量
    private static int eyeNum;
    
    //实例属性
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }

    public static int getEyeNum() {
        return eyeNum;
    }

    public static void setEyeNum(int eyeNum) {
        Person.eyeNum = eyeNum;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public static void main(String[] args) {
        Person p1=new Person("小明",18);
        Person p2=new Person("小爱",10);
        Person.setEyeNum(2);
}

1.Person类存在的时,eyeNum就已经存在了,默认初始值为0
在这里插入图片描述
2.分别创建两个Person对象并实例化
在这里插入图片描述
我们可以看到eyeNum不属于对象的实例,而是属于这个类,当创建Person的对象的时候,并没有为eyeNum这个属性分配内存。若创建对象时,一开始没有为name初始化,那么默认初始化为null。
3.为eyeNum赋值
在这里插入图片描述

2.5.2局部变量

       局部变量:定义在一个形参变量,方法的内部的定义的变量,代码块定义的变量。
       与成员变量不同,局部变量在定义后,必须显式地初始化才能使用,系统不会为局部变量初始化。这意味着当局部变量定义后,系统不会为其分配内存空间,只有显式初始化之后才会为这个变量分配空间。局部变量不属于任何类也不属于任何类的实例,它们总是保存在所在方法的栈内存中。如果这个变量是基本类型,那么直接把这个变量的值保存早该变量对应的内存中;如果是引用类型,局部变量是个引用类型的变量,这个变量存的是一个地址,通过地址找到对应的值。

       栈内存的变量无需系统垃圾回收,栈内存的变量往往是随着方法或代码块的运行结束而结束的。因此,局部变量的作用域是从初始这个变量开始,直到该方法或代码块运行完成而结束。
       因为局部变量只保存基本类型值,或者对象的引用,因此局部变量所占的内存大小一般相对较小。


static int i=0;
public static void main(String[] args) {
	for (;i<4;i++){
	    System.out.println(i);
	}
}

public static void main(String[] args) {
      int i=0;
      for (;i<4;i++){
          System.out.println(i);
      }
}
public static void main(String[] args) {
    for (int i=0;i<4;i++){
        System.out.println(i);
    }
}

       上面这三段代码结果是一样的,但是程序的效果差距很大,第三个最符合软件开发的规范。对于一个循环变量而言,只需在循环体内有效即可,因此把这个变量在循环体内定义始化最合适(代码块)。因此,能使用局部变量的,尽量使用局部变量,而不是成员变量。
       定义成员变量需要考虑的几点:
       1. 如果定义的变量是用于描述某个对象固有的属性,例如人的身高,每个人必定有一个身高,即每个对象有这些信息,我们应该定义为成员变量;不同的人的身高可能不同,即每个对象对应身高值不同,因此我们可以定义为成员属性,且为对象属性。如果定义的变量是用于描述某个类固有的属性,例如眼睛,目前所有人的眼睛是2只,因此这个定义为成员变量,为类属性。
       2. 如果某个信息需要在一个类的多个方法之间共享,则定义为成员变量。
       3. 用于保存每个类或者实例的状态信息。




2.6隐藏和封装

       封装是面向对象的三大特征之一,它是指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息,而是通过该类提供的方法来实现对内部信息的操作和修改。
       使用封装可以达到的目的:

  1. 隐藏实现类的细节。
  2. 让使用者通过预先设定的方法访问数据,可以在该方法添加逻辑限制,从而限制不合理访问。
  3. 可进行数据检查,从而有利保证对象的完整性。
  4. 便于修改,提高代码的可维护性。
  5. 将对象属性和实现细节隐藏起来,不允许外部直接访问。
  6. 把方法暴露出来,让方法来操作这些访问属性。

       因此封装有两个含义:该隐藏的隐藏起来,该暴露的暴露出来,这个两个方面可以通过控制符实现。

2.6.1控制符
private
default
protected
public
从左到右级别依次增大
控制符
1. private:用private修饰的成员或者方法表示,该成员或者方法只能在该类中使用。
2. default:默认就是没有用修饰符修饰,包访问权限,处于同一个包下的类可以访问。
3. protected:子类访问权限。使用这个修饰符只能是继承这个类的子类能访问。使用这个修饰符,通常是希望子类能够重写父类。
4. public:公共访问权限,这是最松的一个,这个类可以被所有的类访问。
  • 注意:一个类可以没有修饰符,但是一旦使用public这个修饰符,就要求文件名与类名相同。
 privatedefaultprotectedpublic
同一个类中
同一个包中
子类中
全局范围内

       一个类通常就是一个小的模块,我们应该只把外界需要的内容暴露出来,其他的隐藏起来。进行程序设计时,应该尽量避免对一个类的数据被直接访问,模块设计追求高内聚(尽量将模块的数据,功能实现细节隐藏在模块内部独立完成,不允许外部直接干预)、低耦合(仅暴露少量的方法供外部调用)。正如我们的内存条,内存条的数据及其实现细节被完全隐藏在内存条里面,外部设备(例如,主机板)只能通过内存条的金手指(提供一些方法供外部调用)来和内存条交互

2.6.2 package和import

       包是什么呢?我们首先想一个问题,假设你们班有两个同学同名都叫张山,老师点名的时候要怎么区分呢?这时候老师可以在前面加一个限定区分,例如大张山,小张山。
       同样的,在Sun公司开发JDK的时候,成千上万的类,每个类有不同的用处,可以保证每个类名都不一样吗?显然这不现实,因此也就有了包。Java引入包(package)机制,提供类的多层命名空间。用于解决命令冲突问题。

格式
package 包名
package应该是在源文件的第一句分注释性语句,一个源文件只能指定一个包。
选择文件夹
src
lee
其他包
Hello.java
src
lee
其他包
Hello.class

       Java的包机制需要保证两个方面:1.源文件里使用package语句指定包名;2.class文件必须放在对应的路径下。

为避免不同公司之间的类名重复问题,Sun公司建议使用公司Internet域名倒写来作为包名,例如:公司域名为yeeyu.org,则所有类的建议放在org.yeeyu包及其子包下。

如果需要使用其他包下的类创建实例,可以带上这个类所在的包名,例如:lee.Hello hello=new lee.Hello();;或者使用import导入这个包import lee.Hello;,这样使得代码更加简洁。

import导入包使得代码更简洁,如果需要导入这个包下的某个类,可以加上具体的类名,例如import lee.Hello;;如果想要导入lee包下的所有类我们可以这样import lee.*;




2.7继承

       Java通过extends实现继承的,实现继承的类称为子类,被继承的类称为父类(基类、超类)。

格式:
修饰符 class 类名 extends 父类{}

        extends翻译过来是扩展的意思,这也体现了子类和父类的关系:子类是对父类的扩展,子类是一种特殊的父类。例如:Apple类继承Fruit类,准确的说,Apple类扩展了Fruit类。

重写父类方法
        子类扩展父类,子类是一个特殊的父类。大部分时候,子类以父类为基础,增加额外的新属性和方法。还有一种情况是:子类需要重写父类的犯法,例如,我们都知道鸵鸟属于鸟类,但是鸟类有个共有的属性,就是都能飞,但是鸵鸟不能飞,这时候鸵鸟类(子类)就需要重写鸟类(父类)飞这个方法。
        方法的重写,也叫覆盖。方法的重写遵循“两同两小一大”:两同,是指方法名相同,形参列表相同;两小,是指子类返回类型应该比父类更小或者相等、子类方法抛出的异常应该比父类抛出的异常更小或者相等;一大,是指子类的访问权限一个比父类方法更大或者想等。
        当方法覆盖之后,子类对象将无法访问父类中被覆盖的方法。但是还可以在子类中调用父类的被覆盖的方法。
        覆盖的方法不能是

单继承
在Java中是单继承的,extends只能继承一个类;但是实际上,Java运行间接继承无限个父类,也就是说直接继承只能由一个,

super
super是java的一个关键字,它是父类对象的默认引用。

        Java在创建一个对象的实例时会隐式地创建一个父类对象。因此,只要有一个子类对象,就一定会有一个对应的父类对象。在子类方法中,使用super引用时,super总是指向作为该方法调用者的子类对象所对应的父类对象。this总是指向该方法的对象,而super则指向this对象的父对象。

this引用
super引用
子类方法
方法的调用者--子类对象
父类对象

注意:super关键字和this关键字都不能出现在static的方法中,static修饰的方法是属于类的,该方法的调用者可能是类,而不是类的对象。

        如果子类定义了和父类相同的方法名,但是参数列表不同,这时候就是1子类与父类之间的方法重载。
        如果子类定义了与父类相同的属性名,会发生子类覆盖父类属性的情况。子类可以通过super关键字访问父类被覆盖的属性。
        一般情况下,子类不会调用父类的构造器,有时候也需要调用父类,这时候可以使用super关键字。在子类的构造器中调用父类的构造器,super();但是必须放在构造器的第一行。

public class Bird{
}
public class Ostrich extends Bird{
	public Ostrich(){
		super();//调用父类构造器
	}
}

        子类的构造器,第一行既没有使用super调用父类的构造器,也没有使用this调用该类的重载构造器,那么子类的构造器将会隐式调用父类的无参构造器。不管怎样,父类的构造器总是会在子类构造器执行之前调用,不仅如此,还会执行父类的父类的构造器。java.lang.Object是所有类的父类,因此,所有类执行之前,总是先调用这个Object类。
        我们可能没有感受到java.lang.Object被调用。因为自定义的类从未有显示调用过java.lang.Object类的构造器,即使是显示调用,java.lang.Object也只有一个默认的无参构造器。当系统执行java.lang.Object是,并为输出任何内容,所以我们感受不到java.lang.Object类的构造器。




2.8多态

        Java引用变量有两个类型:一个编译时的类型,一个是运行时的类型。编译时的类型右声明该变量时使用的类型决定,运行时的类型由实际赋给该变量的对象决定。如果编译时的类型与运行时的类型不一致,就会出现所谓的多态(Polymorphism)。

public class Father {
    String name;
    int age;
    int book=6;

    public void base(){
        System.out.println("父类的普通方法base");
    }
    public void test(){
        System.out.println("父类被覆盖的方法test");
    }
}
public class Son extends Father{
    // 重新定义一个book实例覆盖父类的book实例属性
    private String book="十万个为什么";

    //覆盖父类方法
    @Override
    public void test() {
        System.out.println("子类覆盖父类的方法");
    }

    public static void main(String[] args) {
        System.out.println("-------------Father father=new Father();------------");
        //编译时类型与运行时类型相同,不存在多态
        Father father=new Father();
        //输出6
        System.out.println("book:"+father.book);
        //调用的是Father类的base和test方法
        father.base();
        father.test();

        System.out.println("-----------------Son son=new Son();-----------------");
        //编译时类型与运行时类型相同,不存在多态
        Son son=new Son();
        System.out.println("book:"+son.book);
        //base方法是继承父类,调用的是Son的test方法
        son.base();
        son.test();

        System.out.println("-----------Father f2=new Son();------------");
        //编译时类型和运行时类型不同,存在多态
        Father f2=new Son();
        System.out.println("book:"+f2.book);
        f2.base();
        f2.test();
    }
}
运行结果
运行结果

        上面创建了3个引用对象,前面两个对象运行时和编译时类型都一样不存在多态。而f2对象,运行时类型是Father,编译时类型是Son,调用test方法时,实际上执行的是子类覆盖的方法。运行时调用的是该引用变量的方法时,方法的行为却是子类类型,执行同一个方法时,呈现的是不一样的状态,这就是多态。

        引用变量在编译阶段,只能调用编译时类型所具的方法,而运行时,只能执行运行时类型所局域的方法。

        子类是一种特殊的父类,因此将子类赋值给父类是,不需要转型,由系统自动完成向上转型。
        对象的属性不具有多态性,例如上面的f2.book输出的Father类里面的book属性值。
        引用类型的强制性转换,只能把一个父类变量类型转为子类,但是不能把两个没有关系的类强制性转换,这样产生编译的错误。考虑到强制性类型转换可能会发生异常,我们转换之前可以使用instanceof运算符来判断是否可以成功转换,从而避免ClassCastException异常。




2.9继承和组合

        继承是实现类重用的重要手段,带来方便的同时,还带来了一个坏处:破幻封装。组合也是实现类重用的方式,采用组合方式实现类重用则提供更好的封装性。

2.9.1继承

:         子类扩展父类时,子类可以从父类继承得到属性和方法,如果访问权限允许,子类可以直接访问父类的属性和方法,相当于子类可以直接复用父类的属性和方法,确实很方便。
        与此同时,继承也严重破坏了父类的封装性,在提到封装时:每个都应该封装它内部的信息和实现细节,而值暴露必要的方法给其他类使用。继承使得子类可以访问父类的属性和方法,造成子类和父类的严重耦合。子类可以访问父类的方法和属性,导致子类可以恶意篡改父类的方法(重写)。
        设计父类遵循的规则:
: 1. 尽量因此父类的内部数据,尽量把父类的所有属性设计成private访问类型,不让子类直接访问父类属性。
: 2. 不要让子类随意访问、修改父类方法。父类中辅助其他的工具方法设置为private访问类型;如果父类中方法需要被外部调用,以public修饰,但又不希望子类重写这个方法,使用final修饰符修饰;如果希望被子类重写,不希望被其他类自由访问,使用protected修饰。
: 3. 不用再父类构造器中调用被子类重写的方法。

public class Father {
    public Father() {
        test();
    }

    public void test(){
        System.out.println("父类被覆盖的方法test");
    }
}

情况1:

public class Son extends Father{
    private String name;

    //覆盖父类方法
    @Override
    public void test() {
        System.out.println("子类覆盖父类的方法:"+name);
    }

    public static void main(String[] args) {
      Son s=new Son();
    }
}
运行结果
在这里插入图片描述
情况2:
public class Son extends Father{
    private String name;

    //覆盖父类方法
    @Override
    public void test() {
        System.out.println("子类覆盖父类的方法:"+name+"  :"+name.length());
    }

    public static void main(String[] args) {
      Son s=new Son();
    }
}
运行结果:
在这里插入图片描述

        如果父类的方法在父类构造器中被调用,而子类又重写了这个方法,那么在创建子类对象时,这个重写的方法用到子类的属性,这时候这个属性为null,可能会发生空指针异常。在上面的代码我们可以看出,在创建第二个son时,在子类中写的方法使用了name.length(),此时name还没有被初始化,为null,这时候就会产生空指针异常。因为在创建子类对象时,会先执行父类的构造器,如果父类调用的方法被子类重写,则调用子类被重写后的方法。

        什么时候使用继承呢?除了子类是一种特殊父类,还需具有下面两个条件之一:
  1. 子类需要额外增加属性,不仅仅是属性值得改变。例如:从Person类派生出Student类,Person类中没有grade(年级)这个属性,而Student需要grade属性。
  1. 子类需要增加自己独有的行为(包括新增方法和重写父类方法)。例如:从Person类中派生出Teacher类,Teacher类还有一个teach方法是Person所没有的,是独有的行为。

4.9.2组合

        将一个类(A)嵌套到另一个类(B),(A)作为它(B)的一个部分。

public class Son1{
    //将原来的父类嵌套到子类中,作为子类的一个组合部分
    private Father father;
    
}
public class Son2{
    //将原来的父类嵌套到子类中,作为子类的一个组合部分
    private Father father;
    
}

使用组合关系实现复用时,若有多个类嵌套这个类,需要创建多个该对象,是不是意味着使用组合关系时,系统开销更大?

:不会。我们想想继承时,当创建一个子类对象,必定会在子类创建之前创建一个与之对应的父类对象(隐式)。因此使用组合实现复用时并不会加大系统开销。




2.10使用初始化块

        初始化块在Java类中可出现的第四种成员(三种:属性,方法,构造器),一个类可以有多个初始化块。相同类型的初始化块间有顺序:从上到下,先定义的初始化块先执行,后面定义的后面执行。

public class Person {

    {
        //初始化块1
        int a=10;
    }

    {
        //初始化块2
        int b=1;
    }
    //实例属性
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    
}

        执行顺序:先执行初始化代码块1,然后初始化代码快2,然后才是构造器。
        初始化代码块和构造器相似都是初始化操作。但是初始化块没有名字,也就没有标识,因此无法通过类、对象调用初始化代码块,初始化只在创建Java对象时隐式执行,而且在构造器之前执行。

一个类中,虽然可以定义多个初始化代码块,但是很多情况下,定义多个是没有意义的,这些代码块都是会隐式执行的,因此我们完全可合并。

        初始化放在构造器还是初始化代码块呢?如果两个构造器初始化内容一样,一段相同的代码出现两次,我们一般都不希望出现这种情况。每个构造器初始化内容一样,我们就可以把初始化内容放在初始化代码块中,这样更好体改初始化代码的复用,提高整个程序的可维护性。

静态初始化块

        如果使用static修饰初始化代码块。这个初始化代码块就变成了静态初始化块,不仅仅会这些本类的静态初始化块。还会追溯到java.lang.Object类(如果它包含静态初始化块),先执行java.lang.Object类的静态初始化块,然后执行其父类静态初始化块…最后才执行这个类的静态初始化块,经过这一系列的过程才完成初始化。

Java系统加载并初始化某个类时,总是抱着该类所有父类(包括直接父类和间接父类)全部加载并初始化。

        静态是初始化块和声明静态属性时所指定的初始值都是该类的初始化代码,他们执行的顺序与源程序中顺序相同。下面这段代码两次对a进行赋值,最终结果是9。这说明static int a=9;是后面执行的。

public class TestStatic{
	static{
		a=6;
	}
	static int a=9;
	public static void main(String[] args){
		//最终结果输出9
		System.out.println(a);
	}
}

:         当JVM第一次主动加载某个类时,系统会在类准备阶段为所有静态属性分配内存;在初始化阶段,则负责初始化这些静态属性,初始化静态属性就是执行类初始化代码,或者声明类属性时指定的初始值,他们执行顺序与源代码的排列顺序相同。

下一篇:面向对象下(五)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值