Java 面向对象细节


前言

      Java是面向对象的程序设计语言,类是面向对象的重要内容,可以把类当成一种自定义类型,可以使用类来定义变量,这种类型的变量统称为引用变量,也就是说,所有类是引用类型。

二、类与对象

2.0、类的定义与对象的使用

2.0.1、类的语法格式

      类中static修饰的成员不能访问没有static修饰的成员。

      定义类的语法格式: 【修饰符】 class 类名
      定义构造器的语法格式: 【修饰符】 构造器名 (形参列表)
      定义成员变量的语法格式: 【修饰符】 类型 成员变量名 【=默认值】
      定义方法的语法格式: 【修饰符】 方法返回值类型 方法名 (形参列表)

      修饰符可以省略,也可以是public、private、protected、static、final。其中public、private、protected三个最多只能出现一个,但可以与static、final组合起来修饰成员变量。
      构造器是一个类创建对象的根本途径,构造器名必须和类名相同,构造器不需要定义返回值类型。如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器。一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器。
      field:成员变量、字段或域。属性(property)在Java中指的是setter和getter方法。比如说某个类具有age属性,意味着该类包括setAge()和getAge()两个方法。可以参照下图:
Little_Ant

2.0.2、对象的创建和使用

      创建对象的根本途径是构造器,通过 new 关键字来调用某个类的构造器即可创建这个类的实例。如果访问权限允许,类里定义的方法和成员变量都可以通过类或实例来调用。类或实例访问方法成员变量的语法是:类.类变量|方法,实例.实例变量|方法,其中类或实例是主调者,用于访问该类或该实例的成员变量或方法。

      大部分情况下,定义一个类就是为了重复创建该类的实例,同一个类的多个实例具有相同的特征,而类则是定义了多个实例的共同特征。因此类不是一种具体存在,实例才是具体存在。

2.0.3、Java堆内存与栈内存

      当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁。因此,所有在方法中定义的局部变量都是放在栈内存中的(包括基本类型变量和对象引用变量);而在程序中创建一个对象时:这个对象本身将被保存到运行时数据区中,以便重复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使在方法结束后,这个对象还可能被另一个引用变量所引用(在方法的参数传递时很常见)。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收器才会在合适的时候回收它。

      如果堆内存中的数组不再有任何变量指向自己,则这个数组将成为垃圾,该数组所占的内存将会被GC回收。因此,为了让垃圾回收机制回收一个数组所占的内存空间,可以将数组变量赋为null,也就切断了数组引用变量和实际数组之间的引用关系,实际的数组也就成了垃圾。这个道理对于对象也是同样适用的,还可以调用Runtime对象的 gc()或 System.gc()等方法来建议系统进行垃圾回收(但也不能确定系统立即会进行垃圾回收)。

      假设 Person 为一个类,对于语句:Person p = new Person(); 这个语句产生了两个东西,它们占用了两块不同的内存,一块在栈内存中,另一块在堆内存中。变量 p 的类型是 Person,它是一个引用数据类型,它被放在栈内存中;而由 p 指向的刚刚创建的 Person 类型实例(new Person())被放在了堆内存中。类似于 c 语言中的指针,p 中封装的是新创建对象的首地址,所以只需要通过操作符 . 来访问对象的实例变量和方法就好了。

2.0.4、this引用

      this关键字总是指向调用该方法的对象。this 的最大作用就是让类中的一个方法,访问该类中的另一个方法或实例变量。通常情况下可以省略 this 前缀,但实际上这个 this 仍然是存在的。

2.1、static修饰符

      关键字static:用它修饰的方法或成员变量,表明该成员属于这个类本身,而不属于该类的单个实例,故将static修饰的成员变量或方法称为类变量、类方法。 不用static修饰的成员属于该类的单个实例,而不属于该类。故将不使用static修饰的成员变量或方法称为实例变量、实例方法(Instance 方法)。

      静态变量的其它说明:①随着类的加载而加载,可通过“类.静态变量”的方式进行调用。②早于对象的创建。③由于类只会加载一次,则静态变量在内存中只有一份
      实例变量:当创建了类的多个对象时,每个对象都独立的拥有一套类中的非静态属性。当修改其中某个对象的非静态属性时,不会导致其他对象中同样的属性值的修改。
      静态变量:当创建了类的多个对象时,多个对象共享同一个静态变量。当通过某个对象修改静态变量时,会导致其他对象调用此静态变量时,也发生变化。

      如果一个属性是需要被多个对象所共享的,不会因为对象的不同而发生变化的,可以设置其为静态属性。
      简单的来讲:如果一个方法仅需要访问静态属性变量或方法,那么推荐将其设置为静态方法。也需要在具体场景下具体分析。

      由于static有静态的意思,也将static修饰的成员变量或方法称为静态变量、静态方法。将不使用static修饰的成员变量或方法称为非静态变量、非静态方法。 静态成员不能直接访问非静态成员。

2.2、Java方法

      Java的方法不能独立存在,它必须属于一个类或一个对象,且不能像函数那样独立执行,执行方法时必须使用类或对象来作为调用者,即所有方法都必须使用“类.方法”或“对象.方法”来实现。在同一个类里不同方法之间相互调用时,如果被调用的方法是普通方法,则默认采用this作为调用者(调用方法通过this引用自己当前所在的对象);如果被调用的方法是静态方法,则默认采用类作为调用者。

      方法的参数传递机制:Java的实参传入方法的方式只有一种,即值传递。所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到影响。

2.2.1、方法重载overload

      Java 允许同一个类中定义多个同名方法,只要形参列表不同就行。如果一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。确定一个方法需要三个要素:1,调用者 2,方法名 3,形参列表,这三要素也构成了方法签名(signature)。方法重载即:同一个类中方法名相同,但是参数列表不同。

2.2.2、方法重写override

      当子类在继承父类时,可以定义一些新的特征,以及修改父类的方法,也被称为对父类方法的覆盖。

      所谓方法的重写是指子类中的方法与父类中继承的方法有完全相同的返回值类型、方法名、参数个数以及参数类型。

      如果子类将父类中的方法重写了,调用的时候肯定是调用被重写过的方法,那么如果现在一定要调用父类中的方法该怎么办呢?此时,通过使用super关键就可以实现这个功能,super关键字可以从子类访问父类中的内容,如果要访问被重写过的方法,使用“super.方法名(参数列表)”的形式调用。

2.3、Java变量

      类的成员变量初始化:一般来讲,无须显式初始化,只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化,默认初始化基本类型为0,引用类型为null。 而局部变量必须初始化之后才能使用。

      关于成员变量的使用规则:如果某个变量表示的是这个类的固有信息,应采用类变量,如果表示的是某个实例的固有信息,应采用实例变量。比如:对于Person类来说,眼睛的个数应设置为类变量,因为所有实例都具有两只眼睛。而身高体重信息应该设置为实例变量,因为每个实例的身高体重信息不尽相同。

      关于局部变量的使用规则:应该在程序中尽可能地缩小局部变量的作用范围,局部变量的作用范围越小,它在内存中停留的时间就越短,程序运行性能就越好。因此,能使用代码块局部变量的地方,就不要使用方法局部变量。(这点目前意识的不是很清楚)

      当Java创建一个对象时,系统先为该对象的所有实例变量分配内存(前提是该类已经被加载过了),接着程序开始对这些实例变量执行初始化,初始化顺序为:先执行初始化块或声明实例变量时指定的初始值(这两个地方指定初始值的执行允许与它们在源代码中的排列顺序相同),再执行构造器里指定的初始值。

2.4、访问控制符private default protected public

      访问控制符级别:private < default < protected < public 。其中default为不加任何访问控制符的访问控制级别。private(当前类访问权限):用它修饰的成员只能在当前类的内部被访问。default(包访问权限):用它修饰的成员或外部类可以被相同包下面的其他类访问。protected(子类访问权限):用它修饰的成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。通常采用protected修饰一个方法时,是希望其子类来重写该方法的。public(公共访问权限):用它修饰的成员就可以被所有类访问,不管访问类与被访问类是否处于同一个包中,是否具有父子继承关系。

      外部类的访问级别通常有两种:public和默认(default)。使用public来修饰即表示该类可以被所有类使用,使用default来修饰表示该类只能被同一个包中的其他类使用。

      如果一个Java源文件里定义的所有类都没有使用public修饰,则这个Java源文件的文件名可以是一切合法的文件名;但如果一个Java源文件里定义了一个public修饰的类,则这个源文件的文件名必须与public修饰的类的类名相同。

      Java默认为所有源文件导入 java.lang包下的所有类,包括System和String等等类。

      在不使用 import 的情况下:创建一个 HashMap 的语句为: java.util.Map a =new java.util.HashMap<>();

2.5、类的设计

      高内聚低耦合,是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。目的是使程序模块的可重用性、移植性大大增强。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低。内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事,它描述的是模块内的功能联系;耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。一个类常常就是一个小的模块,应该只让这个模块公开必须让外界知道的内容,而隐藏其他一切内容。进行程序设计时,应尽量避免一个模块直接操作和访问另一个模块的数据 ,模块设计追求高内聚(尽可能把模块的内部数据、功能实现细节隐藏在模块内部独立完成,不允许外部直接干预)、低耦合(仅暴露少量的方法给外部使用)。

2.6、包装类

      包装类实现基本类型与字符串之间的转换:
      将字符串类型的值转换为基本类型的值:1,利用包装类提供的parseXxx(String s)静态方法;2,利用包装类提供的valueOf(String s)静态方法       例如:int i1=Integer.parseInt(“1234”); [推荐使用]       int i2=Integer.valueOf(“1234”);

      将基本类型的值转换为字符串:1,采用String类的多个重载valueOf()方法       例如:String s=String.valueOf(234 / 3.14f / true);2,将基本类型变量与空字符串"“进行拼接,系统会自动将基本类型变量转换为字符串。      例如:String s=5 + “” ;结果s的值为"5” 。

2.7、toString()

      toString() 方法默认返回一个对象实现类的“类名 + @ + hashCode”

2.7、"=="和equals()

2.7.1、“==”

      "=="表示两个引用类型变量指向同一个对象,也就是这两个引用类型变量(指针)的值是否相等;对于两个基本类型变量,且都是数值类型(不一定要求数值类型严格相同),则只要两个变量的值相等,就返回true。

      例如:String str1 = new String(“hello”); String str2 = new String(“hello”) 如果用"==“判断 str1 和 str2 是否相等时,返回false。(new String(“hello”)语句执行时,JVM会先使用常量池来管理“hello”直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。这是“hello”与new String(“hello”)的区别。)
      int it=65; float f1=65.0f; char ch=‘A’; 这三者用”=="来判断都会返回true。


2.7.2、equals()

      Object类提供的实例方法equals()用来判断两个对象是否相等与采用"==''没有区别。故其没有太大的实际意义,如果希望采用自定义的相等标准,则可重写equals方法来实现

      而String类已经重写了equals方法,只要两个字符串所包含的字符序列相同,通过equals方法比较将返回true,否则返回false。

      重写Object类的equals方法示例: 假设当前类为 Person 类,成员变量为String类型的 name 和String类型的 idstr 。

	public boolean equals(Object obj)
	{
		//如果两个对象为同一个对象
		if(this==obj)
			return true;
		//只有当obj为Person对象时
		if(obj!=null&&obj.getClass()==Person.class)
		{
			Person personObj=(Person)obj;
			//自定义标准只要两个对象的Stirng变量idstr值相等即可。
			if(this.getIdstr().equals(personObj.getIdstr()))
				return ture;
		}
			return false;
	}

      说明:上面程序判断 obj 是否为 Person 类的实例时,如果采用 instanceof 来判断的话,当前面的对象是后面类的实例或其子类的实例都将返回 true。而如果是 obj 是 Person 的子类的话,采用 instanceof 也会返回 true,而对父类对象和子类对象来重载 equals 方法判断是否相等没有太大意义,一般情况下,我们的目的是对同一个类的两个对象(不能包含子类)来比较。所以改为使用 obj.getClass()==Person.class 比较合适。

2.8、单例类(Singleton类)

      如果一个类只能创建一个实例,则这个类被称为单例类。在一些特殊的场景下:要求不允许自由创建该类的对象,而只允许为该类创建一个对象。所以需要采用 private 来修饰该类的构造器。

      根据封装的原则:当构造器被隐藏起来的话,就需要提供一个 public 方法作为该类的访问点,用于创建该类的对象,并且该方法必须使用 static 修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。

      当然,该类还需要存储已经创建的对象,用来确保只能创建一个对象。为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,故该成员变量必须使用 static 修饰。

class Singleton{
    private static Singleton instance;//存储曾经创建的实例。
    private Singleton(){};//隐藏构造器方法
    //getInstance方法用来返回一个Singleton实例
    //并且方法实现里加入自定义控制,用来保证只产生一个Singleton对象
    public static Singleton getInstance()
    {
        if(instance==null)
            instance=new Singleton();
        return instance;
    }
}
public class SingletonTest {
    public static void main(String[] args) {
        Singleton s1=Singleton.getInstance();
        Singleton s2=Singleton.getInstance();
        System.out.println(s1==s2);//结果为true
    }
}

2.9、final修饰符

      final 用来修饰类、变量和方法,类似于C#里的 sealed 关键字,用来表示它修饰的类、方法和变量不可改变。final 修饰的变量一旦获得了初始值,该 final 变量的值就不能被重新赋值。

2.9.1、final成员变量

      final 修饰的成员变量必须由程序员显式地指定初始值。否则这些成员变量的值就会一直为系统默认分配的 0、‘0’、false和null。
      final类变量:必须在静态初始化块或声明该变量时指定初始值,二者之中选其一。
      final实例变量:必须在普通初始化块、声明该实例变量时或构造器中指定初始值,三者之中选其一。

public class FinalVariableTest {
    final int a=6;//final实例变量第一种指定初始值的方法
    final String str;
    final int c;
    final static double d;
    final static int e=9;//final类变量第一种指定初始值的方法
    static{
        d=5.6;//final类变量第二种指定初始值的方法
    }
    
    {
        str="hello";//final实例变量第二种指定初始值的方法
    }
    
    public FinalVariableTest()
    {
        c=78;//final实例变量第三种指定初始值的方法
    }
}

2.9.2、final局部变量

      final 局部变量在定义时可以指定默认值,或不指定默认值。指定默认值的在后面代码中就不能再赋值了;在不指定默认值的情况下,可以在后面代码中对该 final 变量赋初始值,但只能一次,不能再次赋值。

2.9.3、final基本类型变量和引用类型变量

      但使用 final 来修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但是对于引用类型变量而言,它保存的仅仅是一个引用,final 只保证这个引用类型变量所引用的地址不会改变,即保证一直引用同一个对象,但是这个对象本身是可以被改变的

2.9.4、可执行“宏替换”的final变量

      对于一个 final 变量来说,不管它是类变量、实例变量或局部变量,只要其满足三个条件:使用 final 修饰符修饰;在定义该 final 变量时指定了初始值;并且该初始值可以在编译时就被确定下来
      如果代码有语句:final int a = 5; System.out.println(a); 那么在此处打印的变量 a 其实根本不存在,当程序执行时,直接将其转换为 System.out.println(5);       即在此处的变量 a 就相当于一个宏变量,编译器会把程序中所有用到该变量的地方直接替换为该变量的值。类似于 #define a 5

2.9.5、final 方法

      final 修饰的方法不能被重写,当不希望子类重写父类的某个方法时,可以使用 final 来修饰。在 Java 的 Object 类中存在一个 final 方法:getClass(),因为 Java 不希望任何类重写该方法。相反对于 toString() 和 equals() 方法,是允许子类重写的。

2.9.6、final 类和不可变类

      final 修饰的类不能有子类。不可变类是指创建该类的实例后,该实例的实例变量是不可改变的。Java提供的8个包装类和 java.lang.String类都是不可变类。在不可变类的内部就是采用 final 修饰的成员变量,导致成员变量只要被初始化一次之后,就不能再被改变。

2.10、抽象类

      存在抽象方法的类只能被定义为抽象类,但是抽象类里可以没有抽象方法。抽象方法和抽象类用 abstract 来修饰。

      抽象方法:在父类中只有方法签名,没有方法体,例如:对于方法 public void test () { } 来说,对应的抽象方法格式为 public abstract void test (); ,抽象方法意味着这个方法必须由子类提供实现(重写)。

      抽象类不能被实例化,只能当做父类被子类继承。抽象类的构造器不能创建实例,主要用于被子类调用。

      抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会大致保留抽象类的行为方式。如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留个其子类实现,这就是一种模板模式,模板模式是一种十分常见且简单的设计模式之一。

      模板模式:1,抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现。2,父类中可能包含需要调用其他系列方法的方法,这些被调方法既可以由父类实现,也可以由其子类实现。父类里提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,而必须依赖于其子类的辅助。

2.11、接口

2.11.1、接口概述

      抽象类是从多个类中抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的“抽象类” - 接口。接口定义的是多个类共同的公共行为规范,它不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类必须提供某些方法,提供这些方法的类就可以满足实际需要。接口体现的是规范和实现分离的设计思想,采用这种面向接口的耦合,会尽量降低各模块之间的耦合,可以提供更好的可扩展性和可维护性。这些行为是与外部交流的通道,这就意味着接口里通常是定义一组公用方法,这组公用方法就是后面会提到的普通方法,也即抽象实例方法(public abstract)。

      Java 9 中接口的定义格式:
      【修饰符】 interface 接口名 extends 父接口1,父接口2…
      {
            零个到多个常量定义…
            零个到多个抽象方法定义…
            零个到多个内部类、接口、枚举定义…
            零个到多个私有方法、默认方法或类方法定义…
      }

      接口定义的是一组规范,所以接口里不能包含构造器初始化块定义。接口里可以包含成员变量(只能是静态变量)、方法(只能是抽象实例方法,类方法,默认方法(Java 8)或私有方法(Java 9))、内部类(内部接口,枚举)定义。

      接口中的静态常量默认采用 public static final 修饰符,不管定义时是否指定该修饰符,系统都会自动使用 public static final 来修饰。

      接口中的内部类(内部接口,枚举)默认采用 public static 修饰符,不管定义时是否指定该修饰符,系统都会自动使用 public static 来修饰。

      在接口中定义的方法只有4种:抽象方法、类方法、默认方法(Java 8)和私有方法(Java 9),如果不是定义默认方法、类方法或私有方法,系统自动为普通方法增加 abstract 修饰符,即为抽象方法;

      定义接口里的普通方法时,不管是否采用 public abstract 修饰符,接口里的普通方法总是使用 public abstract 来修饰。接口里的普通方法不能有方法实现(方法体);但类方法、默认方法、私有方法都必须有方法实现(方法体)。

      具体的,见如下示例:

public interface Output {
    int MAX_CACHE_LINE=50;  //成员变量只能是常量,即 public static final 类型的


    void out();//普通方法只能是 public 的抽象方法,即 public abstract 类型的。
    void getData(String msg);


    default void print(String... msgs)//默认方法需要采用 default 来修饰
    {
        for(String m:msgs)
            System.out.println(m);
    }
    default void test()
    {
        System.out.println("默认的 test() 方法");
    }


    static String staticTest()//接口中的类方法用 static 来修饰
    {
        return "这是一个类方法";
    }


    private void foo()// 私有方法用 private 来修饰, 注意:这是从JDK9开始才允许采用 private 修饰符的。 私有方法常用来作为工具方法
    {
        System.out.println("一个私有方法");
    }
    private static void bar()
    {
        System.out.println("一个私有静态方法");
    }
}

      在接口中的普通方法定义了这个接口的规范,如上例: 只要某个类具备了 取得数据(getData)或输出数据(out) 的功能就认为它是一个实现了Output接口的设备,而不关心具体的实现细节。

      在接口中的默认方法因为没有用 static 来修饰,所以不能直接使用接口来调用默认方法,需要使用接口的实现类的实例来调用这些方法。(接口的默认方法可以认为就是一般意义上的实例方法)

      在接口中的类方法可以直接调用接口来实现。

      在接口中的私有方法的主要作用就是作为工具方法,为接口中的默认方法或类方法提供支持。(Java 9 更新的内容,为了解决当两个默认方法(类方法)中包含一段相同的实现逻辑时, 需要将这段实现逻辑抽取成为一个工具方法,而这个工具方法应该是被隐藏的,所以就有了私有方法)

2.11.2、接口的继承

      接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似的,子接口会获得父接口里定义的所有抽象方法、常量。

      继承多个父接口的接口如下:

interface Product{
    int getProductTime();
}
interface Output {
    int MAX_CACHE_LINE=50;  //成员变量只能是静态常量,即 public static final 类型的
    void out();//普通方法只能是 public 的抽象方法,即 public abstract 类型的。
    void getData(String msg);
    default void print(String... msgs)//默认方法需要采用 default 来修饰
    {
        for(String m:msgs)
            System.out.println(m);
    }
    default void test()
    {
        System.out.println("默认的 test() 方法");
    }
    static String staticTest()//接口中的类方法用 static 来修饰
    {
        return "这是一个类方法";
    }
}

public class Printer implements Product,Output {
    private ArrayDeque<String> printData=new ArrayDeque<>();
    
    public void out()
    {
        while(!printData.isEmpty())
        {
            System.out.println("Print: "+printData.pollFirst());
        }
    }
    public void getData(String msg) {
        if(printData.size()>=MAX_CACHE_LINE)
            System.out.println("FUll. can't get data.");
        else
            printData.offerLast(msg);
    }
    public int getProductTime()
    {
        return 5;
    }
    public static void main(String[] args)
    {
        Output o=new Printer();
        o.getData("abc");
        o.getData("def");
        o.out();

        o.print("赵","钱","孙","李");// o 继承了Output中的默认方法
        o.test();

        Product p =new Printer();
        System.out.println(p.getProductTime());
    }
}

2.11.3、接口和抽象类

      接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(通过其内的抽象方法);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何来调用这些服务(通过其内的抽象方法)。当在一个程序中使用接口时,接口是多个模块之间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。

      从某种程度上来看,接口制定了系统各模块应该遵循的标准,因此一个系统的接口不应该经常改变。一旦接口被改变,可能会导致系统中大部分类都需要改写。

      抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),还需要更进一步地完善。

2.11.4、面向接口编程

      接口体现的是一种规范和实现分离的思想,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。所以,很多软件架构设计理论都倡导“面向接口”编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。

      1,工厂模式:
      假设程序中有一个 Computer 设备需要组合一个输出设备,此时应该采用 Output 接口来组合还是采用 Printer 实现类呢?答案应该是采用 Output 接口,因为不能确保 Printer 实现类永远不会迭代升级,相比之下,Output 接口的持久性要更好一些,并且组合一个 Output 接口可以将实现与规范相分离。示例如下:

//此 Computer 与 Printer 类分离,只与 Output 接口耦合。
public class Computer {
    private Output out;//组合一个接口,亦或者调用一个接口提供的服务:输入(getData)和输出(out)
    public Computer(Output out)
    {
        this.out=out;
    }
    public void keyIn(String msg)//输入字符串
    {
        out.getData(msg);
    }
    public void print()//输出
    {
        out.out();
    }

}

//此 OutputFactory 类只是来实现接口,创建了一个 Output 对象为 Printer,
//如果后续对 Printer 升级改进了得到 BetterPrinter 类,完全只需要修改一行代码如下所示,而不需要去修改 Computer 类的内容。
public class OutputFactory {
    public Output getOutput()
    {
        return new Printer();
        //return new BetterPrinter();
    }

    public static void main(String[] args)
    {
        OutputFactory a=new OutputFactory();
        Computer b=new Computer(a.getOutput());
        b.keyIn("abc");
        b.print();
    }

}

      2,命令模式:
      如果某个方法需要完成某一种行为,但是这个行为的具体实现又无法确定,必须等到执行该方法时才能确定。这个时候也可以采用接口来实现。假设有个方法需要遍历某个数组的数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。

      采用 Command 接口来定义一个方法,用这个方法来封装“具体的处理行为”。示例如下:

public interface Command {
    //此方法定义的 process 方法用来封装对数组元素的处理行为;
    void process(int element);
}

//定义具体的行为,其实完全可以将下面两个实现放在两个java文件中,而放在此仅为方便起见。
class CmdPrint implements Command{
    public void process(int element)
    {
        System.out.println(element);
    }
}
class CmdAddOneAndPrint implements Command{
    public void process(int element)
    {
        System.out.println(element+1);
    }
}
public class ProcessArray{
    //本类用来对数组进行处理,它采用了某种行为来对数组进行处理(由 Command 中的 process 方法来决定)。
    public void process(int[] target,Command cmd)
    {
        for(int tmp:target)
            cmd.process(tmp);
    }
    public static void main(String[] args)
    {
        int[] array={1,2,3,4,5,6};
        ProcessArray pa=new ProcessArray();
        Command cmdPrint=new CmdPrint();
        Command cmdAddOneAndPrint=new CmdAddOneAndPrint();
        pa.process(array,cmdPrint);
        pa.process(array,cmdAddOneAndPrint);
    }
}

      故,接口体现的是规范和实现分离的设计思想,采用这种面向接口的耦合,会尽量降低各模块之间的耦合,可以提供更好的可扩展性和可维护性。

2.12、内部类

      当一个类被定义在另一个类的内部,这个定义在其他类内部的类就被称为内部类,包含内部类的类也被称为外部类。

      内部类的作用:
1,内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
2,内部类被当为外部类的一个成员,同一个类的成员之间可以互相访问,包括内部类成员可以直接访问外部类的私有数据;但外部类不能访问内部类的实现细节,例如内部类的实例成员(非静态成员)。
3,匿名内部类适合创建那些仅需要一次使用的类。

      外部类能使用的修饰符为 default 和 public。而内部类还可以使用 private、protected 和 static 。
      外部类的上一级程序单元是包,所以它只有 2 个作用域:同一个包内或任何位置。因此只需要两种访问权限-包访问权限和公开访问权限。default(缺省)修饰符表示该类可以被同一个包下面的其他类来访问。
      内部类的上一级单元是外部类,所以它具有 4 个作用域:同一个类、同一个包、父子类和任何位置。(对应于: private、default、protected 和 public)而修饰符 static 则表示为静态内部类。

      大部分情况下,内部类都作为外部类的一个成员,与成员变量、方法、构造器和初始化块相似。而局部内部类和匿名内部类则不是类成员。

2.12.1、非静态内部类

      成员内部类分两种:静态内部类(采用 static 修饰)和非静态内部类(没有采用 static 修饰)。非静态内部类不能拥有静态成员(静态方法、静态成员变量和静态初始化块)。

      下例说明了:内部类中可以直接访问外部类的成员,而外部类中却不能直接访问内部类的实例成员。其原因在于非静态内部类对象存在时,外部类对象必然存在;而外部类对象存在时非静态内部类对象不一定存在。

      1,内部类如果存在变量名冲突,那么内部类成员变量采用 this.varName,外部类成员采用 外部类名.this.varName。

      2,若要在外部类中访问内部类的实例成员时,需要先创建一个内部类对象。

public class Cow {
    private double weight;
    public Cow(){};
    public Cow(double weight){
        this.weight=weight;
    }

    private class CowLeg{
        private double length;
        private String color;
        private double weight;
        public CowLeg(){};
        public CowLeg(double length,String color){
            this.length=length;
            this.color=color;
        }
        public void info(){
        	double weight=0;
            System.out.println("color: "+ color+",length: "+length);
            System.out.println("weight of local: "+weight);
            System.out.println("weight of leg: "+this.weight);
            System.out.println("weight of cow: "+Cow.this.weight);//内部类可以直接访问外部类的实例成员(包括私有成员变量)
        }
    }

    public void test(){
        //length=8.9; 外部类方法中不能直接访问内部类的实例成员(即任何非静态成员)
        //info();  
        CowLeg c=new CowLeg(1.2,"red");//若想访问其示例成员,需要先创建一个内部类对象,包括访问或修改private成员。
        c.length=2.3;
        c.color="blue";
        c.info();
    }
    
    public static void main(String[] args){
        Cow c=new Cow(350);
        c.test();
    }
}

2.12.2、静态内部类

      由 static 来修饰的内部类属于外部类本身,而不属于外部类的某个对象。称其为静态内部类,它是外部类的一个静态成员。

      1,静态内部类可以包含静态成员和非静态成员。静态内部类作为外部类的一个静态成员,在静态内部类中,不能访问外部类的实例成员,只能访问外部类的类成员。

public class Cow {
    private int p = 5;
    private static int p1 = 9;
	
    static class StaticClass {
        private static int age;
        public void accessP() {
            //System.out.println(p);//无法访问。
            System.out.println(p1);
        }
    }
}

      2,外部类可以通过类名来访问静态内部类的成员,对于静态内部类的类成员,采用静态内部类的类名来调用;对于静态内部类的实例成员,采用静态内部类对象来调用。

public class Cow {
	
    static class StaticClass {
        private static int age=18;
        private int num=10;
    }
    public void info()
    {
        //System.out.println(age);   can't access
        //System.out.println(num);   can't access
        System.out.println(StaticClass.age);
        System.out.println(new StaticClass().num);
    }
    public static void main(String[] args){
        Cow c=new Cow();
        c.info();
    }
}

      在接口中定义内部类或子接口,默认采用 public static 修饰该内部类或子接口。

2.12.3、使用内部类

      如果内部类可以在外部类以外被访问,那么内部类完整的类名为:OuterClass.InnerClass 。

      1.1,在外部创建一个非静态内部类对象如下,注意:非静态内部类的构造器必须由其外部类对象来调用。

class Out {
	//此非静态内部类为包访问权限
    class In {
        public In(String msg){};
    }
}
public class CC{
    public static void main(String[] args){
        Out.In innerClassName=new Out().new In("haha");//和下面创建一个内部类对象等效。
        Out outer =new Out();
        Out.In in=outer.new In("hh");//非静态内部类的构造器必须由其外部类对象来调用
    }
}

      1.2,由非静态内部类生成一个子类时,必须有一个外部类对象存在,不然无法调用内部类的构造函数,示例如下:

class Out {
	//此非静态内部类为包访问权限
    class In {
        public In(String msg){};
    }
}
public class CC extends Out.In{
    public CC(Out out){
        out.super("hello");  //这里的 super 指的是 In 的构造函数
    }
    public static void main(String[] args){
        CC c=new CC(new Out());
    }
}

      2.1,在外部创建一个静态内部类对象时,不需要额外创建外部类对象了。

class Out {
    //此静态内部类为包访问权限
    static class In {
    }
}
public class CC{
    public static void main(String[] args){
        Out.In in=new Out.In();
    }
}

      2.2,由静态内部类生成一个子类时,无需提供外部类对象。示例如下:

class Out {
    //此静态内部类为包访问权限
    static class In {
        public In(String a){};
    }
}
public class CC extends Out.In{
    public CC(String a){
        super(a);//不需要再传入 Out 类对象了。
    }
    public static void main(String[] args){
        CC c=new CC("haha");
    }
}

2.12.4、局部内部类

      局部内部类即定义在某个方法内的内部类,它仅在方法内有效(局部变量),因为局部内部类不能在方法以外使用,故其不能使用访问控制符和 static 修饰符修饰。

      局部内部类的定义变量、创建实例和派生子类都只能在局部内部类所在的方法中进行。如下:

public class CC{

    public static void main(String[] args){
        class InnerClass{// 局部类
            int a; //包访问权限
            private double aa;//私有权限
            //暂未提供方法
        }
        class InnerSub extends InnerClass{
            int b;
        }
        InnerSub c=new InnerSub();
        c.a=0;
        c.b=1;
    }
}

      但在实际开发中很少用到局部内部类,因为它的作用域太小了,只能在当前方法中使用。

2.12.5、匿名内部类

      匿名内部类适合创建那种只需要使用一次的类,即创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类无法重用。(因此匿名内部类不能是一个抽象类,即必须实现所有抽象方法,不然无法创建一个实例。当然若是有需要的话也可以重写父类中的普通方法;也无法在匿名内部类中定义一个构造器,因为没有类名,但初始化工作可以采用实例初始化块来完成。)
      如果想要对某个接口实现类重复使用,应该创建一个独立类。

      匿名内部类必须继承但最多一个父类、或实现最多一个接口。格式为: new 实现接口() | 父类构造器(实参列表){ //匿名内部类的类体部分}

      1,最常见的创建匿名内部类的方式是需要创建某个接口类型的对象。

interface Product{
    double getPrice();
    String getName();
}
public class CC{

    public void test(Product p)
    {
        System.out.println(p.getPrice() +" "+ p.getName());
    }
    public static void main(String[] args){
        CC c=new CC();
        c.test(new Product(){
            public double getPrice(){
                return 3.14;
            }
            public String getName(){
                return "pi";
            }
        });
        
    }
}

      2,使用匿名内部类来继承抽象父类:

abstract class Device{
    private String name;
    public abstract double getPrice();
    public Device(){};
    public Device(String name){
        this.name=name;
    }
    public String getName(){
        return name;
    }
}
public class CC{

    public void test(Device p)
    {
        System.out.println(p.getPrice() +" "+ p.getName());
    }
    public static void main(String[] args){
        CC c=new CC();
    /*    c.test(new Device("abcde"){
            public double getPrice(){
                return 314.15;
            }
        }); // 调用带参数的构造函数。 */
        c.test(new Device() {
            @Override
            public double getPrice() {
                return 314.15;
            }
            public String getName(){  //重写父类的 getName 方法
                return "圆";
            }
        });
    }
}

      3,局部内部类、匿名内部类访问的局部变量必须使用 final 修饰。Java 8 之前只有 final 局部变量才可以被这二者访问,Java 8 之后,如果一个被访问的局部变量哪怕不是 final 类型,也会等价于 final 类型,不能对其重新赋值。见下例:

abstract class Device{
    public abstract double getPrice();
}
public class CC{
    public void test(Device p)
    {
        System.out.println(p.getPrice());
    }
    public static void main(String[] args){
        int a=9;
        CC c=new CC();
        c.test(new Device(){
            public double getPrice(){
                System.out.println(a);
                return 314.15;
            }
        }); 
        //a=8;  会报错。
    }
}

2.13、Lambda表达式与函数式接口使用简介

      Lambda表达式自 Java 8 开始增加,它允许使用更简洁的代码来创建只有一个抽象方法的接口的实例。只有一个抽象方法的接口也被称为函数式接口,函数式接口也可以有自己的默认方法和类方法。 匿名内部类的使用范围是 Lambda 表达式的超集(不局限于仅一个抽象方法),并且匿名内部类还可以应用于抽象类。

      通过下面这个例子来比较一下匿名内部类和 Lambda 表达式的差异:

interface Process{
    void processInt(int element);
}
public class CC {
    void test(int[] a,Process b)
    {
        for(int i:a){
            b.processInt(i);
        }
    }
    public static void main(String[] args){
        int[] array={1,2,3,4};
        CC c=new CC();
/*        c.test(array, new Process() {
            @Override
            public void processInt(int element) {
                System.out.print(element+1 +" ");
            }
        }); */
        c.test(array,(int element)->{
            System.out.print(element+1 +" ");
        });
    }
}

      匿名内部类和 Lambda 表达式的实现效果一模一样,但是 Lambda 表达式要更简洁一些。它不需要写 new Xxx() 、不需要指出重写的方法名和返回值类型,而只需要给出重写的用圆括号括起来的形参列表,、-> 和用大括号括起来的方法体即可。

      1,如果形参列表中只有一个参数,那也可以不用圆括号括起来。
      2,中间采用箭头(->)连接。
      3,方法体(代码块):如果方法体中只有一条语句,可以省略大括号;如果方法体中只有一条 return 语句,return 关键字也可以省略。

      如下表:

接口中的抽象方法原型Lambda 表达式
int add1(int a,int b);(a,b)->{ int c=a+b; return c;}【通用格式】
void taste();()->System.out.println(“Good”)
void fly(String wea);wea->{System.out.println(“Good”);System.out.println(“Good”)}
int add(int a,int b);(a,b)->a+b

2.14、枚举类简介

      如果一个类的对象是有限且固定的,比如季节类,称其为枚举类。枚举类采用关键字 enum 来定义。枚举类是一种特殊的类,它一样可以有自己的成员变量、方法,包括去实现接口,定义自己的构造器。一个 Java 源文件最多只能定义一个 public 访问权限的枚举类,且该源文件名也必须和该枚举类的类名相同。(enum 和 calss、interface 地位相同)

      使用 enum 定义、非抽象的枚举类默认会使用 final 来修饰。(即不可变类,不能派生出子类)
      枚举类的所有实例必须在枚举类的第一行列出,并且系统会自动添加 public static final 修饰。

      枚举类默认提供了一个values() 方法,可用来遍历所有的枚举值。示例:

public enum Season {
    //第一行列出 4 个枚举实例
    SPRING,SUMMER,FALL,WINTER; //这四个枚举值代表了本枚举类所有可能的实例。
}
public final class CC {
    public void judge(Season a){
        switch(a){
            case SPRING:
                System.out.println("春天");
                break;
            case SUMMER:
                System.out.println("夏天");
                break;
            case FALL:
                System.out.println("秋天");
                break;
            case WINTER:
                System.out.println("冬天");
        }
    }
    public static void main(String[] args){
        for(Season s:Season.values())
            System.out.println(s);
        new CC().judge(Season.SPRING);
    }
}

2.15、对象和垃圾回收

      当程序创建对象、数组等引用类型实体时,系统都会在堆内存中为之分配一块内存区,对象就保存在这块内存区中,当这块内存不再被任何引用变量引用时,这块内存就变成了垃圾,等待垃圾回收机制进行回收。

      1,垃圾回收机制值负责回收堆内存中的对象,不会回收任何物理资源(如数据库连接、网络I/O等)
      2,程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行。当对象永久性地失去引用后,系统就会在合适的时候回收它所占的内存
      3,在垃圾回收机制回收任何对象之前,总会先调用它的 finalize() 方法,该方法可能会使对象重新复活(让另一个引用变量重新引用该对象),从而导致垃圾回收机制取消回收。

      一、对象在内存中的状态

      可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用对象的实例变量和方法。

      可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的 finalize() 方法进行资源清理。如果系统在调用 finalize() 方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则进入不可达状态。

      不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的 finalize() 方法后依然没有使该对象变为可达状态,那该对象永久地失去引用,最后变为不可达状态。处于不可达状态的对象,系统才会真正回收该对象所占用的资源。

    public static void main(String[] args){
        String a=new String("haha"); // 1
        a=new String("yingyingying"); // 2
    }

      在 1 处,引用变量 a 指向 haha 对象,该行代码结束后,haha 对象处于可达状态。 当程序执行完代码 2 之后,haha 对象处于可恢复状态,yingyingying 对象处于可达状态。

      一个对象可以被局部变量引用,可以被其他类的类变量引用,也可以被其他对象的实例变量引用。被类变量引用的,只有该类被销毁后,该对象才会进入可恢复状态;被实例变量引用的,只有该对象被销毁后,该对象才会进入可恢复状态;

      二、强制垃圾回收

      系统何时调用 finalize() 方法对失去引用的对象进行资源清理,何时它会变为不可达状态,何时回收它所占用的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,决不能控制它何时被回收。

      这里的强制垃圾回收指的是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。但在大多数情况下,总会有一点效果的。强制系统垃圾回收的方式有两种,任选其一即可:

      调用 System 类的 gc() 静态方法:System.gc()
      调用 Runtime 对象的 gc() 实例方法:Runtime.getRuntime().gc()

      下面的程序运行之后,没有任何输出,直到程序退出,系统都不曾调用 CC 对象的 finalize() 方法。

public final class CC {
    public void finalize(){
        System.out.println("清理资源中...");
    }
    public static void main(String[] args){
        for(int i=0;i<4;i++){
            new CC();
        }
    }
}

      在 for 循环中添加代码 System.gc(); 即可看到 4 次调用 CC 对象的 finalize() 方法来进行垃圾回收。

public final class CC {
    public void finalize(){
        System.out.println("清理资源中...");
    }
    public static void main(String[] args){
        for(int i=0;i<4;i++){
            new CC();
            System.gc();
            //Runtime.getRuntime().gc();  效果一样
        }
    }
}

      三、finalize() 方法

      在垃圾回收机制回收某个对象所占用的内存之前,通常要求程序调用适当的方法来清理资源,在没有明确指定清理资源的情况下,Java 会采用默认机制 finalize() 方法来清理资源。它是 Object 类中的实例方法,原型为:protected void finalize() throws Throwable

      在 finalize() 方法返回后,对象消失,垃圾回收机制才开始执行。

      任何 Java 类都可以重写 Object 类的 finalize() 方法,在该方法中清理该对象占用的资源。但如果程序终止之前始终没有进行垃圾回收,则不会调用已失去引用的对象的 finalize() 方法来清理资源。而垃圾回收机制何时调用 finalize() 是透明的,只有当程序认为需要更多额外内存时,垃圾回收机制才会进行垃圾回收。因此当内存空间充裕,且失去引用的对象占用内存空间很小,则永远也不会进行垃圾回收,不会调用 finalize() 方法。

      一般地:1,永远不要主动调用某个对象的 finalize() 方法,将其交给垃圾回收机制调用。2, finalize() 何时被调用、是否被调用具有不确定性。3,JVM 执行可恢复对象的 finalize() 方法时,有可能将该对象或系统中其他对象重新变为可达状态。4,JVM 执行 finalize() 方法出现异常时,垃圾回收机制不会报告异常,程序继续执行。

      所以最好不要将清理某个类中资源的操作放在 finalize() 方法中,因为它不一定会执行。当然,有别的方法用于清理资源。

      关于 finalize() 方法将可恢复状态的对象变为可达状态的示例如下:

public final class CC {
    private static CC c=null;
    public void info(){
        System.out.println("清理资源中...");
    }
    public void finalize(){
        c=this;//让类变量 c 指向本程序中的可恢复对象 new CC();
    }
    public static void main(String[] args){
        new CC();//该对象创建完毕之后立即进入可恢复状态
        System.gc();//通知系统进行资源回收   1
        System.runFinalization();//强制垃圾回收机制调用可恢复对象的 finalize() 方法。  2
        //Runtime.getRuntime().runFinalization(); 这个也可以
        c.info();
    }
}

      该程序会有输出。表示可恢复对象 new CC() 被类变量 c 所引用,从而可以调用 info() 方法,得到输出:清理资源中…

      如果取消 1 处的通知系统资源回收代码后,系统通常不会立即进行垃圾回收(内存并没有紧张),也就不会调用 new CC() 对象的 finalize() 方法,而在 c.info() 中,c 为 null ,从而引发空指针异常。

      如果取消 2 处的强制垃圾回收机制调用可恢复对象的 finalize() 方法,系统仅执行 System.gc() 方法,由于 JVM 垃圾回收机制的不确定性,通常并不会立即调用可恢复对象的 finalize() 方法,所以在 c.info() 中,c 仍然可能为 null ,从而引发空指针异常。

总结

      本文简单介绍了一下 Java 面向对象的细节知识,由于自己有一些基础,故有些地方就一笔带过了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值