[JAVA] Java面向对象之final、abstract抽象、和变量生命周期

376 阅读15分钟

Java面向对象之final、abstract抽象、和变量生命周期

Java面向对象之final、abstract抽象、和变量生命周期

final修饰符

final是最终、不可修改的意思, 在Java中它可以修饰非抽象类,非抽象方法和变量。但是需要注意的是:构造方法不能使用final修饰,因为构造方法不能够被继承。下面,咱们就来一一看看吧!

使用final关键字修饰类

先考虑下图的代码例子:

final class

代码显示错误,无法从SuperClass继承,编译器提示删除final关键字;删除final关键字后,代码正确无误。

代码正确无误

由此可得出:final修饰的类:,表示最终的类,,即该类不能再有子类,不能再被继承。只要满足以下条件就可以考虑把一个类设计成final类:

  1. 在设计之初就考虑不进入继承体系的类。
  2. 出于安全考虑,类的实现细节不允许被拓展和修改。比如:基本数据类型的包装类就是一个典型的例子。
  3. 该类不会再被拓展。

java里final修饰的类有很多,比如八大基本数据类型的包装类(Byte,Character、Short、Integer、Long、Float、Double、Boolean)和String等。

// Byte
public final class Byte extends Number implements Comparable<Byte> { }
// Character
public final class Character implements java.io.Serializable, Comparable<Character> { }
// Short
public final class Short extends Number implements Comparable<Short> { }
// Integer
public final class Integer extends Number implements Comparable<Integer> { }
// Long
public final class Long extends Number implements Comparable<Long> { }
// Float
public final class Float extends Number implements Comparable<Float> { }
// Double
public final class Double extends Number implements Comparable<Double> { }
// Boolean
public final class Boolean implements java.io.Serializable, Comparable<Boolean> { }
// String 
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { }

使用final关键字修饰方法

如果用final关键字修饰方法呢?先考虑以下的代码:

final 方法

若是用final修饰方法,继承该方法时会报编译错误;删除该关键字后,doWork()可被继承,代码编译通过;final修饰的方法为最终的方法,该方法不能被子类覆盖,故也不能使用方法重写。那么什么样的情况下方法需要使用final修饰呢?

  1. 在父类中提供了统一的算法骨架,不允许子类通过方法覆盖来修改其实现细节, 此时使用final修饰。比如在模板方法设计模式中。
  2. 在构造器中调用的方法(初始化方法),此时一般使用final修饰。这也是构造器不能被继承的原因。

注意: final修饰的方法了,子类可以调用,但是不能覆盖(重写)。

类常量:使用final关键字修饰的字段

常量分类:

  1. 字面值常量(直接给出的数据值/直接量);比如:整数常量1,2,3,小数常量3.14,布尔常量false,true等。
  2. final关键字修饰的常量。

final关键字修饰的常量

通过上述代码,不难看出,final关键字修饰的字段无法被修改。通常开发中,我们建议final修饰的常量名用大写字母表示,多个单词之间使用下划线(_)连接:如:

public static final String USER_NAME = "用户名";

且在Java中多个修饰符之间是没有先后关系的,以下的三种修饰符排列顺序都是ok的:

public static final 
或者 
public final static 
亦或者 
final static public 

final修饰的变量是最终的变量,常量;该变量只能赋值一次,也只能在声明时被初始化一次,不能被修改。在使用时需注意:

  1. final变量必须显式地指定初始值,系统不会为final字段初始化。
  2. final变量一旦赋予初始值,就不能再被重新赋值。
  3. 常量名规范:常量名符合标识符,单词全部使用大写字母,如果是多个单词组成,多个单词之间使用下划线(_)连接。全局静态常量: public static final 修饰的变量,直接使用类名调用即可。

final修饰的引用类型变量到底表示引用的地址不能改变,还是其存储的数据不能改变

  • 修饰基本类型变量:表示该变量的值不能改变,即不能用“=”号重新赋值。
  • 修饰引用类型变量:表示该变量的引用的地址不能变,而不是其存储的数据内容不能变,其存储的数据内容是可以被修改的。

什么时候使用常量

  • 当在程序中,多个地方使用到共同的数据,而且该数据不会改变,此时可以将其定义全局的常量;
  • 一般的,在开发中我们会专门定义一个常量类,专门用来存储常量数据。

为何要使用final修饰符呢?在继承关系中最大弊端就是会破坏封装,子类能访问父类的实现细节,,而且可以通过方法重写(方法覆盖)的方式修改方法的实现细节。且 final还是是唯一可以修饰局部变量的修饰符。

抽象方法和抽象类

考虑如下的案例:求圆(Circle)、矩形(rectangle)的面积

求圆(Circle)、矩形(rectangle)的面积 初始代码设计

上述代码设计是存在问题的:

  1. 每一个图形都有面积。但是不同图形求面积的算法是不一样的,也就是说,每一个图形的子类都必须去重写getArea方法,如果不覆盖,应该编译报错,无法计算其面积。
  2. 在图形类(Graph)中定义了getArea方法,该方法不应该存在方法体,因为不同图形子类求面积算法不一样,父类是不存在计算面积的方法的,故无法提供方法体。

案例:求圆(Circle)、矩形(rectangle)的面积 引入抽象的设计

案例:求圆(Circle)、矩形(rectangle)的面积 引入抽象的设计

抽象方法

使用abstract关键字修饰且没有方法体的方法,称为抽象方法。其特点是:

  1. 使用抽象abstract关键字修饰,方法没有方法体,留给子类去实现/覆盖其实现细节。
  2. 抽象方法修饰符不能是private 和 final以及static,因为抽象方法是要被重写的;
  3. 抽象方法必须定义在抽象类或接口中。接口中的方法魔人都是使用public abstract 修饰的;

一般会把abstract写在方法修饰符最前面,一看就知道是抽象方法;当然如果不这样写也没错。

抽象类

使用abstract关键字修饰的类,称为抽象类。其特点是:

  1. 抽象类不能创建实例,也就是不能使用new创建一个抽象类对象,即使创建出抽象类对象,调用了抽象方法,也无法实现功能,因为抽象方法没有方法体。
  2. 抽象类可以不包含抽象方法,倘若包含,哪怕是一个,该类也必须作为抽象类,抽象类可以包含普通方法,可以给子类调用;抽象类是有构造器的,且其子类构造器必须先调用父类构造器。
  3. 若子类没有实现/覆盖父类所有的抽象方法,那么子类也得作为抽象类(抽象派生类)。
  4. 构造方法不能都定义成私有的,否则不能有子类,因为子类构造器无法调用其构造器(创建子类对象前先调用父类构造方法)。
  5. 抽象类不能使用final修饰,因为其必须有子类重写其抽象方法,抽象方法才能得以实现。
  6. 抽象类是不完整的类,需作为父类,由子类实现其功能细节,功能才能得以实现。

抽象类在命名时,一般使用Abstract作为前缀,让调用者见名知义,看类名就知道其是抽象类。

抽象类中可以不存在抽象方法,这样做虽然没有太大的意义,但是可以防止外界创建其对象,所以我们会发现有些工具类没有抽象方法,但却是使用abstract来修饰类的。

普通类有的成员(方法、字段、构造器),抽象类本质上也是一个类,故其都有。抽象类不能创建对象,但抽象类中是可以包含普通方法的。

变量生命周期

程序中的变量是用来存储数据的,其又分为常量和变量两种,关于变量的详情可以查看我的另一篇文章:[JAVA] Java 变量、表达式和数据类型详解。定义变量的语法:

数据类型 变量名 = 值;

变量根据在类中定义位置的不同,分成两大类

成员变量: 全局变量/字段(Field),是定义在类中,方法作用域外的变量;可以先使用后定义(使用在前,定义在后)。

  1. 类成员变量:使用static修饰的字段。
  2. 实例成员变量:也称为对象变量,即没有使用static修饰的字段。

**局部变量:**变量除了成员变量,其他都是局部变量,主要体现在方法内,方法参数,代码块内;局部变量必须先定义而后才能使用。

  1. 方法内部的变量。
  2. 方法的形参。
  3. 代码块中的变量,一对{}中的变量。

变量的初始值:变量只有在初始化后才会在内存中开辟空间。

成员变量: 默认是有初始值的。

成员变量的初始值

局部变量: 没有初始值。所以必须先初始化才能使用,而且其初始化是在方法执行开始时才进行的。

变量的作用域:变量根据定义的位置不同,也决定了各自的作用域是不同的,最直观的就是看变量所在的那对花括号{},也就是离得最近的那对{}。成员变量的作用域在整个类中都有效。局部变量的作用域在开始定义的位置开始,到紧跟着结束的花括号为止。

变量的生命周期

变量的作用域指的是变量的可使用的范围,只有在这个范围内,程序代码才能访问它。当一个变量被定义时,它的作用域就确定了。变量的作用域决定了变量的生命周期,作用域不同,生命周期就不一样。

变量的生命周期指的是一个变量被创建并分配内存空间开始,到该变量被销毁并清除其所占内存空间的过程。

package 关键字

在开发中,一个项目会有成百上千个Java文件,如果所有的Java文件都在一个目录中,那么管理起来就会很痛苦,很难想象这样的项目会是什么样子。在Java中,引入了称之为包(package)的概念。即:关键字:package ,专门用来给当前Java文件设置包名(也就是命名空间)。其语法格式如下:

package 包名.子包名.子包名; 

必须把package语句作为Java文件中的第一行代码,在所有代码之前。

package 语句和java编译

在编译java文件时的编译命令为:

javac -d . Hello.java

如果此时Hello.java文件中没有使用package语句,表示在当前目录中生成字节码文件。运行时也不需要考虑包名。

如果此时Hello.java文件中使用了package语句,此时表示在当前目录中先生成包名目录,再在包名目录中生成字节码文件。运行命令如下:

 java 包名.类名;

package命名

  • a.自定义的包名不能以java开头,会和java语言基础类库冲突。
  • b.包名必须遵循标识符规范/全部小写。
  • c.企业开发中,包名由公司域名倒写来决定。
  • d.如果域名是以数字开头的,不符合规范,可以考虑使用下划线_开头;但是在Android中,如果package中使用了_,则不能部署到模拟器上。此时,我们也可以使用一个字母来代替_。

package命名格式

package 域名倒写.模块名.组件名;

1.package下的类名:

  • 类的简单名称: PackageDemo.java
  • 类的全限定名称: com._520.hello.PackageDemo.java

2.建议:先定义package名称,再在定义的package内定义类。

import 关键字

当A类和B类不在同一个包中,若A类需要使用到B类中的功能,此时就得让A类中去引入B类。使用import语句,把某个包下的类导入到当前类中。

语法格式:    import 需要导入类的全限定名;

引入后在当前类中,只需要使用类的简单名称即可访问。

如果我们需要引入包中的多个类,我们还得使用多个import语句,要写很多次;此时可以使用通配符(*)

  • import 类的全限定名; 只能导入某一个类。
  • import 包名.子包名.*; 表示会引入该包下的所有的在当前文件中使用到的类。
  • import java.util.*; 表示导入java.util包下的所有类。

注意:编译器会默认导入java.lang包下的类,但是并不会导入java.lang的子包下的类。比如:java.lang.reflect.Method类,此时我们也得使用import java.lang.reflect.Method;来导入Method类。

静态import

**静态import,**静态导入,是指将通过import static导入其他类的静态成员。以下代码实例:

package demo.importdir;
public class StaticDemo {	
    public static final int COUNT = 10;
}

package demo.dir;
import static demo.importdir.StaticDemo.COUNT;
public class StaticImportDemo {	
    public static void main(String[] args) {	
        System.out.println(COUNT);	
    }
}

然后我们对StaticImportDemo反编译,观察JVM是如何处理静态导入的:

import java.io.PrintStream;
import demo.importdir.StaticDemo;
public class StaticImportDemo{	
    public StaticImportDemo() {	}	
    public static void main(String args[]) {	
        System.out.println(StaticDemo.COUNT);	
    }
}

通过上述的反编译代码,不难发现,其实所谓的静态导入也是一个语法糖/编译器级别的新特性,其实在底层也是类名.静态成员去访问的。

所以在企业项目开始中不建议使用静态导入,容易引起字段名,方法名混淆,不利于项目维护。

字段不存在多态

通过对象调用字段,在编译时期就已经决定了调用哪一块内存空间的数据。所以字段不存在覆盖的概念,也就是字段不会有多态特征,在运行时期体现的也会是子类特征。

public class FieldDemo {		
    public static void main(String[] args) {	
        SubClass subClass = new SubClass();	
        System.out.println(subClass.name);	
    }
}

class SuperClass {	
    protected String name = "SuperClass.name";
}

class SubClass {	
    protected String name= "SubClass.name";
}

// 运行结果:SubClass.name

通过运行上述代码,不难发现,当子类和父类存在相同的字段的时候,无论修饰符是什么(即使是private),都会在各自的内存空间中存储数据,字段并没有体现出多态;

其实通过方法重写字面意思也能发现其是针对方法的。所以只有方法才有覆盖的概念,而字段并不会被覆盖。

代码块

什么是代码块:在类或者在方法中,直接使用**"{}"**括起来的一段代码,表示一块代码区域,我们将其称为代码块。代码块里变量属于局部变量,只在自己所在的作用域(所在的{})内有效。根据代码块定义的位置的不同,我们又分成三种形式:

1.局部代码块:直接定义在方法内部的代码块;一般不会直接使用局部代码块,而是会结合if,while,for,try等关键字配合使用,还有匿名内部类,表示一块代码区域。示例如下:

if (true) {	......    }

2.初始化代码块(构造代码块):定义在类中,每次创建对象的时候都会执行,并且是在构造器调用之前先执行本类中的初始化代码块。但其实JVM在处理初始化代码块时是将其移动到构造器中的最前面,从而达到先执行初始化代码块,再执行构造器的功能。

在实际开发中,很少使用初始化代码块;初始化操作会在构造器中进行,如果做初始化操作的代码比较复杂,可以另外定义一个方法做初始化操作,然后再在构造器中调用。

3.静态代码块:使用static修饰的初始化代码块。格式如下:

class StaticDemo {		
    static {    	......    }    
}

静态代码块会在主方法(main方法)执行之前执行,而且只执行一次。在Java中,main方法是程序的入口,静态代码块优先于main方法执行;是因为静态成员是随着字节码的加载而进入JVM中的,但此时此时main方法还没执行,因为main方法需要JVM调用方能执行。

以下是一个代码块的示例:

public class CodeBlockDemo {		
    {		
        System.out.println("执行初始化代码块");	
    }		
    
    public CodeBlockDemo() {		
        System.out.println("执行无参构造器");	
    }		
    
    static {		
        System.out.println("执行静态代码块");	
    }		
    
    public static void main(String[] args) {		
        new CodeBlockDemo();		
        new CodeBlockDemo();		
        new CodeBlockDemo();	
    }
}

其运行结果为:

执行静态代码块
执行初始化代码块
执行无参构造器
执行初始化代码块
执行无参构造器
执行初始化代码
块执行无参构造器

不难发现,调用顺序依次为:静态代码块--》初始化代码块--》构造器,且静态代码块只执行一次。然后再对上述示例代码做反编译:

import java.io.PrintStream;
public class CodeBlockDemo{	
    public CodeBlockDemo()	{		
        System.out.println("执行初始化代码块");		
        System.out.println("执行无参构造器");	
    }	
    
    public static void main(String args[])	{		
        new CodeBlockDemo();		
        new CodeBlockDemo();		
        new CodeBlockDemo();	
    }	
    
    static 	{		
        System.out.println("执行静态代码块");	
    }
    
}

通过反编译结果,发现JVM在处理初始化代码块时是将初始化代码块的代码移动到构造器中的最前面,从而达到先执行初始化代码块,再执行构造器的功能。

完结。老夫虽不正经,但老夫一身的才华