Java面向对象之final、abstract抽象、和变量生命周期
final是最终、不可修改的意思, 在Java中它可以修饰非抽象类,非抽象方法和变量。但是需要注意的是:构造方法不能使用final修饰,因为构造方法不能够被继承。下面,咱们就来一一看看吧!
先考虑下图的代码例子:
代码显示错误,无法从SuperClass继承,编译器提示删除final关键字;删除final关键字后,代码正确无误。
由此可得出:final修饰的类:,表示最终的类,,即该类不能再有子类,不能再被继承。只要满足以下条件就可以考虑把一个类设计成final类:
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修饰方法,继承该方法时会报编译错误;删除该关键字后,doWork()可被继承,代码编译通过;final修饰的方法为最终的方法,该方法不能被子类覆盖,故也不能使用方法重写。那么什么样的情况下方法需要使用final修饰呢?
注意: final修饰的方法了,子类可以调用,但是不能覆盖(重写)。
常量分类:
通过上述代码,不难看出,final关键字修饰的字段无法被修改。通常开发中,我们建议final修饰的常量名用大写字母表示,多个单词之间使用下划线(_)连接:如:
public static final String USER_NAME = "用户名";
且在Java中多个修饰符之间是没有先后关系的,以下的三种修饰符排列顺序都是ok的:
public static final 或者 public final static 亦或者 final static public
final修饰的变量是最终的变量,常量;该变量只能赋值一次,也只能在声明时被初始化一次,不能被修改。在使用时需注意:
为何要使用final修饰符呢?在继承关系中最大弊端就是会破坏封装,子类能访问父类的实现细节,,而且可以通过方法重写(方法覆盖)的方式修改方法的实现细节。且 final还是是唯一可以修饰局部变量的修饰符。
考虑如下的案例:求圆(Circle)、矩形(rectangle)的面积
上述代码设计是存在问题的:
案例:求圆(Circle)、矩形(rectangle)的面积 引入抽象的设计
使用abstract关键字修饰且没有方法体的方法,称为抽象方法。其特点是:
一般会把abstract写在方法修饰符最前面,一看就知道是抽象方法;当然如果不这样写也没错。
使用abstract关键字修饰的类,称为抽象类。其特点是:
抽象类在命名时,一般使用Abstract作为前缀,让调用者见名知义,看类名就知道其是抽象类。
抽象类中可以不存在抽象方法,这样做虽然没有太大的意义,但是可以防止外界创建其对象,所以我们会发现有些工具类没有抽象方法,但却是使用abstract来修饰类的。
普通类有的成员(方法、字段、构造器),抽象类本质上也是一个类,故其都有。抽象类不能创建对象,但抽象类中是可以包含普通方法的。
程序中的变量是用来存储数据的,其又分为常量和变量两种,关于变量的详情可以查看我的另一篇文章: [JAVA] Java 变量、表达式和数据类型详解 。定义变量的语法:
数据类型 变量名 = 值;
成员变量: 全局变量/字段(Field),是定义在类中,方法作用域外的变量;可以先使用后定义(使用在前,定义在后)。
局部变量:变量除了成员变量,其他都是局部变量,主要体现在方法内,方法参数,代码块内;局部变量必须先定义而后才能使用。
变量的初始值:变量只有在初始化后才会在内存中开辟空间。
成员变量: 默认是有初始值的。
局部变量: 没有初始值。所以必须先初始化才能使用,而且其初始化是在方法执行开始时才进行的。
变量的作用域:变量根据定义的位置不同,也决定了各自的作用域是不同的,最直观的就是看变量所在的那对花括号{},也就是离得最近的那对{}。 成员变量的作用域 在整个类中都有效。 局部变量的作用域 在开始定义的位置开始,到紧跟着结束的花括号为止。
变量的作用域指的是变量的可使用的范围,只有在这个范围内,程序代码才能访问它。当一个变量被定义时,它的作用域就确定了。变量的作用域决定了变量的生命周期,作用域不同,生命周期就不一样。
变量的生命周期指的是一个变量被创建并分配内存空间开始,到该变量被销毁并清除其所占内存空间的过程。
在开发中,一个项目会有成百上千个Java文件,如果所有的Java文件都在一个目录中,那么管理起来就会很痛苦,很难想象这样的项目会是什么样子。在Java中,引入了称之为包(package)的概念。即:关键字:package ,专门用来给当前Java文件设置包名(也就是命名空间)。其语法格式如下:
package 包名.子包名.子包名;
必须把package语句作为Java文件中的第一行代码,在所有代码之前。
在编译java文件时的编译命令为:
javac -d . Hello.java
如果此时Hello.java文件中没有使用package语句,表示在当前目录中生成字节码文件。运行时也不需要考虑包名。
如果此时Hello.java文件中使用了package语句,此时表示在当前目录中先生成包名目录,再在包名目录中生成字节码文件。运行命令如下:
java 包名.类名;
package 域名倒写.模块名.组件名;
1.package下的类名:
2.建议:先定义package名称,再在定义的package内定义类。
当A类和B类不在同一个包中,若A类需要使用到B类中的功能,此时就得让A类中去引入B类。使用import语句,把某个包下的类导入到当前类中。
语法格式: import 需要导入类的全限定名;
引入后在当前类中,只需要使用类的简单名称即可访问。
如果我们需要引入包中的多个类,我们还得使用多个import语句,要写很多次;此时可以使用 通配符(*) 。
注意:编译器会默认导入java.lang包下的类,但是并不会导入java.lang的子包下的类。比如:java.lang.reflect.Method类,此时我们也得使用import java.lang.reflect.Method;来导入Method类。
静态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在处理初始化代码块时是将初始化代码块的代码移动到构造器中的最前面,从而达到先执行初始化代码块,再执行构造器的功能。
完结。老夫虽不正经,但老夫一身的才华