Java 基础学习(七)final、static、抽象

1 final 关键字

1.1 final修饰变量

1.1.1 final概述

final单词直译为“最终的“,在Java中可以用来修饰变量、方法和类:

  • final修饰的变量:可以初始化,不能再更改
  • final修饰的方法:不能在子类中重写
  • final修饰的类:不能再被继承派生出子类了

1.1.2 final修饰局部变量

在方法中声明的变量称为局部变量。

局部变量加final修饰以后,只能初始化一次,不能再次更改。因此 final 修饰的变量可以在声明的同时初始化或者在构造函数中初始化。

注意:当引用类型变量被设置为final时候,表示其值就是引用对象的地址值不能再次修改了,但是此时被引用对象的属性或者元素是可以被修改的。

使用final的目的是保护局部变量的值不变,避免在方法运算期间被意外篡改。如果一个需要反复更改的局部变量就不要使用final修饰。

查看如下所示代码:

 

int 类型的变量a添加了 final 修饰符,则被初始化之后将不能被再次赋值。

另外, final 修饰的变量 arr 是int类型的数组,也是只能被初始化一次不能再次被修改。但是因为变量 arr 是引用类型,其值是数组对象的首地址(初始化以后不能再次被修改),即变量arr与数组之间的引用关系不能发生改变了;但是数组的内容是可以改变的。因此可以给数组的元素赋值,但是不能给 arr 重新定义。

1.1.3【案例】final修饰局部变量示例

编写代码,测试 final 修饰的基本类型变量和引用类型变量。

案例示意代码如下:

import java.util.Arrays;
public class FinalDemo1 {
    public static void main(String[] args) {
        final int a;
        a = 8; //第一次为变量赋值,称为初始化
        System.out.println(a); //
        //a = 9; //编译错误,a不能再次被修改
        final int b = 9; //声明变量直接初始化
        System.out.println(b);
        //b = 10; //编译错误,不能再次修改final变量
        //final修饰的引用类型变量:
        //引用类型变量初始为一个地址值后不能再次修改
        final int[] arr = {5, 6};//arr中存储的数组地址不能再次修改类
        //arr引用不能修改,但是被引用对象的内容可以修改
        arr[0] = 9;
        System.out.println(Arrays.toString(arr)); //[9, 6]
        //不可以更改arr变量的值,因为arr是final类型的!
        //arr = new int[8];
        final Ball ball = new Ball();
        ball.d = 10;
        System.out.println(ball.d);
        //不能更换ball的值,也就是地址值
        //ball = null;
    }
}
class Ball{
    int d = 5;
}

1.1.4 final修饰方法的参数

Java中方法参数也是一种局部变量,只是其声明位置是方法参数,在接收到传递参数的时候初始化。

在方法参数上可以使用final修饰。final修饰以后也是初始化以后不能再次修改,由于方法参数是在调用方法传递参数值时候初始化的,所以在方法运行期间方法参数变量的值不能修改了。

使用final修饰方法参数的好处也是保护变量的值,避免在方法运行期间参数变量的值被意外篡改。

查看如下代码示例:

 

方法 test 的第二个参数被声明为 final,在方法内部不能再修改其数值了。

1.1.5【案例】final修饰方法参数示例

定义带两个参数的方法(其中一个参数用 final 修饰),编写代码,测试 final 修饰的参数的使用。

案例示意代码如下:

public class FinalDemo2 {
    public static void main(String[] args) {
        test(5,6);
        test(7,8);
    }
    public static void test(int a, final int b) {
        a = 9;
        //b = 8; //编译错误,不能再次更改变量b
        System.out.println("a:"+a+",b:"+b);
    }
}

1.1.6 final修饰实例变量

在类中声明的对象属性,由于是属于每个对象实例的变量所以也称为“实例变量”。

final可以修饰实例变量,在final修饰实例变量时候必须直接初始化或者在构造器中初始化,并且实例变量也是在初始化以后不能再次改变了。

使用final修饰实例变量的目的也是保护实例变量,使其值在初始化以后不能改变,避免程序的意外篡改。比如如果希望一个对象的唯一ID编号,在初始化以后不能改了,就可以利用final修饰。

在实际开发中很少使用final修饰的实例变量!主要原因是不方便对象的复用。很多Java底层框架都会利用对象池重复使用对象,避免反复创建销毁对象的性能开销,如果对象属性是final的,就无法再次进行赋值重用对象了!

查看如下代码示例:

 

类 Eoo 的实例变量 a 和 b,都声明为 final,在 main 方法中测试发现,创建 Eoo 对象后,可以访问该属性,但是不能修改它。

1.1.7【案例】final修饰实例变量示例

为类定义 final 修饰的实例变量并编写代码测试其特点。

案例示意代码如下:

public class FinalDemo3 {
    public static void main(String[] args) {
        Eoo eoo = new Eoo(8);
        System.out.println(eoo.a); //5
        System.out.println(eoo.b); //8
        //不能再次更改final的实例变量
        //eoo.a = 9;
        //eoo.b = 10;
        Eoo e2 = new Eoo(10);
        System.out.println(e2.a);
        System.out.println(e2.b);
    }
}
class Eoo {
    //final的属性必须初始化
    final int a = 5;
    final int b;
    public Eoo(int b) {
        this.b = b;
    }
}

1.2 final修饰方法

1.2.1 final修饰方法

方法上可以使用final修饰,final的方法在子类中不能被重写修改了。简单理解:final方法不能被重写。final修饰的方法不会影响方法在当前类中的使用,但是如果派生了子类,则在子类中不能重写修改父类中定义的final方法。

final方法的好处是避免被子类使用重写语法修改方法的功能,保护方法的功能是“最终”版本。如果需要保护方法的功能,避免在子类中重写修改,就可以使用final进行声明。

但是在实际工程项目中很少使用final方法,原因是很多框架工具都会采用“动态代理”技术代理(重写)对象的功能,实现灵活的软件功能,如果使用final的方法将直接影响这些框架功能!很多些软件开发企业在编程规范中明确规定:不能声明final方法!

查看如下示例:

 

类 Foo 中的方法 test() 添加了 final 修饰符,可以被子类继承但是不能被子类重写。

1.2.2【案例】final修饰方法示例

为类定义 final 修饰的方法并编写代码测试其特点。

案例示意代码如下:

public class FinalDemo4 {
    public static void main(String[] args) {
        Foo foo = new Foo();
        foo.test();
        SubFoo sf = new SubFoo();
        sf.test();
    }
}
class Foo{
    public final void test() {
        System.out.println("Foo.test()");
    }
}
class SubFoo extends Foo{
    //public void test() { //编译错误,不能重写Foo中的final方法
    //    System.out.println("SubFoo.test()");
    //}
}

1.3 final修饰类

1.3.1 final修饰类

类名也可以使用final修饰,被final修饰的类将不能再派生子类了,也就是终结了类的继承。简单理解:final类不能被继承。

final类的好处和final方法类似,也是可以避免被继承和重写,避免被子类修改功能。Java的很多核心API,都是final类型,这样就保护了这些非常重要的API功能。这些API包括:String、Math、Integer、Double、Long等。这些API都不能派生子类。

在开发中也不允许使用final声明类,原因也是因为声明final类以后,造成很多框架无法采用“动态代理”技术代理扩展对象的功能,很多些软件开发企业在编程规范中明确规定:不能声明final类!

查看如下示例:

 

类Goo添加了 final 修饰符,则不能被继承。

1.3.2【案例】final修饰类示例

定义 final 修饰的类,并编写代码测试其特点。

案例示意代码如下:

public class FinalDemo5 {
    public static void main(String[] args) {
        Goo goo = new Goo();
        goo.test();
    }
}
final class Goo{
    public void test() {
        System.out.println("test()");
    }
}
//class SubGoo extends Goo{ //编译错误,不能继承final类
//}

2 static 关键字

2.1 静态变量

2.1.1 成员变量

作为类的成员,在类体中声明的变量称为成员变量,成员变量有三种:

  • 一种是实例变量:是属于每个对象属性,每个对象中都有一份
  • 一种是静态变量:是属于类的变量,只有一份,全体对象共享的同一份变量
  • 一种是常量:是不变的常数

成员变量示例如下:

 

2.1.2 静态变量

使用static修饰类的成员变量,称为静态变量。静态变量只有一份,是可以被全体对象共享的一份变量,这点与实例变量有着巨大的差异。实例变量是每个对象实例都有一份的变量。而静态变量是无论用类创建多少对象,都始终唯一的变量。

静态变量和类的信息一起存储在方法区,是属于类的变量。

可以用静态变量存储程序中只有一个就够的数据,比如:办公软件项目中的公司名称和公司LOGO,地图应用中的各类图标,只需要加载一份就可以了。多份相同的数据反而会浪费大量的时间和存储空间。

2.1.3 静态变量的访问

因为静态变量是属于类的变量,所以使用“类名.变量名”访问,在类的内部可以省略类名。

静态变量关键点:

  • 静态变量只有一份,可以被全体对象共享
  • 软件中只有一份的数据应该使用static修饰

查看如下示例:

 

类Foo定义了静态变量 b,使用 Foo.b 访问。

2.1.4【案例】静态变量示例

在类中定义静态变量,并编写代码测试其特点。

案例示意代码如下:

public class StaticDemo1 {
    public static void main(String[] args) {
        Soo s1 = new Soo();
        Soo s2 = new Soo();
        s1.a = 8;
        s2.a = 10;
        Soo.b = 11; //使用类名访问静态变量
        System.out.println(s1.a + "," + s1.b); //8,11
        System.out.println(s2.a + "," + s2.b); //10,11
        System.out.println(Soo.b); //11 读取静态变量
    }
}
class Soo{
    int a;        //实例变量、对象属性
    static int b; //静态变量
}

2.1.5 静态变量工作原理

在面试中经常有问起静态变量工作原理的。简单的说就是静态变量在类加载期间在方法区中分配,静态变量是属于类的变量。

静态变量的具体工作原理是:

1、Java源文件经过编译得到字节码文件,每个类编译为一个class文件

2、当执行Java程序时候,每用到一个类Java就会自动将对应的字节码加载到方法区

  • 创建对象时候会自动加载类
  • 访问类的静态属性时候会自动加载类
  • 执行类中的静态方法时候会自动加载类
  • 字节码文件只加载一次

3、如果类中有静态变量,Java就会在加载类期间将其在方法区中分配出来,静态变量也初始化一次,只有一份

4、创建对象时候按照类中声明的实例变量分配对象的属性,每创建一个对象,就会分配一组对象属性。

工作原理如下图所示:

 

2.1.6 static final

Java中同时使用static final关键字声明“常量”,常量用于声明不会变化的量。比如:数学中的圆周率PI、自然常数e,物理学中的光速C,都是常量。在软件开发中也会将固定不变的数值声明为常量。比如程序中给用户的提示信息,一般是固定值,就可以声明为常量。 如下所示:

public class Message {
    public static final String MSG_LOGINFAILED = “用户名或密码错误”;
    public static final String MSG_LOGINSUCC = “登录成功”;
}

2.1.7 常量的细节

软件中不能改变的数据,应该都定义为常量 :同时使用static final修饰,两个关键字的顺序可以调换。

常量必须初始化,其命名建议都是大写字母,多个单词使用下划线隔开,比如 MAX_VALUE。

Java API中提供了很多常量:

  • Math.PI、 Math.E
  • Integer.MAX_VALUE、Integer.MIN_VALUE
  • Long.MAX_VALUE、Long.MIN_VALUE

2.2 静态方法

2.2.1 静态方法

所谓的静态方法就是在方法声明时候添加static关键字。添加了静态关键字的方法称为静态方法,静态方法是属于类的方法。属于类的方法就可以直接使用类名直接引用方法。比如:Math.random()就是一个静态方法。

查看如下示例:

 

对于 Person 类的 add() 方法,它没有用到当前的属性,则可以定义为静态方法。在使用时,使用类名.方法名() 的方式调用。

2.2.2 静态方法的细节

当一个方法其方法体中没有用到任何当前对象的属性时候,则此方法就可以定义为静态方法。相反如果方法用到了当前对象的数据,就不能定义为静态方法。这也是是否在方法前面添加static关键字的原则。由于静态方法与对象数据无关,所以静态方法可以使用类名直接访问。

静态方法和对象方法还有一个区别就是,静态方法没有隐含的局部变量this。而对象方法中是保护隐含局部变量this,对象方法就是通过这个this引用访问了当前对象的属性和方法。由于这个差别,就有一个现象:静态方法不能访问实例变量和对象方法。

因为main方法也是静态方法,所以main方法也不能访问当前类型的实例变量和对象方法。

2.2.3【案例】静态方法示例

在类中定义静态方法,并编写代码测试其特点。

案例示意代码如下:

public class StaticDemo2 {
    public static void main(String[] args) {
        Person.add(7, 8); //用类名调用静态方法
        Person tom = new Person("Tom");
        tom.whoru(); //用对象引用调用对象的方法
    }
}
class Person{
    String name;
    public Person(String name) {
        this.name = name;
    }
    public void whoru() {
        //对象方法中包含隐含局部变量this
        System.out.println("我是"+this.name);
    }
    //如果方法中没有用到当前对象的属性/方法就声明为static
    public static void add(int a, int b) {
        //静态方法中没有隐含局部变量this
        System.out.println(a+b);
    }
}

2.3 static 其他用法

2.3.1 代码块

在类中可以使用{}定义代码块,代码块在创建对象时候按照顺序执行,其功能与构造器类似,可以用于初始化对象属性。代码示意如下:

class Cell{
   int a;
   {
        //代码块,在创建对象时候执行
        a = (int)(Math.random()*8);
   }
}

大多情况下优先使用构造器初始化对象属性,代码块很少被使用,了解即可。

2.3.2 静态代码块

类中使用static修饰的代码块称为静态代码块。

静态代码块在类加载期间执行,因为Java类只加载一次,所以静态代码块也只执行一次。就是因为这个特性,经常用静态代码块初始化类中的静态属性。如:将图片资源定义为静态属性,然后利用静态代码块加载图片文件,初始化静态图片属性。

静态代码块的使用示例如下:

 

2.3.3【案例】静态代码块示例

定义并测试静态代码块,案例示意代码如下:

public class StaticDemo3 {
    public static void main(String[] args) {
        //Java会在创建对象之前自动加载类Circle
        Circle c1 = new Circle();
        Circle c2 = new Circle();
        System.out.println(Circle.angle);
    }
}
class Circle{
    static double angle; //角
    static {//静态代码块,在类加载期间执行,只执行一次
        System.out.println("初始化angle");
        angle = Math.PI * 2;
    }
}

2.3.4 静态导入

Java 8 提供了静态导入语法,用于简化静态资源的编码,其语法为:import static。例如:

import static java.lang.Math.PI;
import static java.lang.Math.sin;
import static java.lang.Math.*;

使用示例如下:

 

3 abstract 抽象

3.1 抽象类

3.1.1 什么是抽象类

使用抽象关键字abstract声明的类是抽象类,抽象类不能直接实例化创建对象。

这个定义看上去非常茫然,究其原因是因为在面向对象设计时候,会利用“泛化”将子类的共同属性和方法抽取出来设计出父类,此时的父类往往是半成品类,只包含部分属性和方法,甚至属性值都没有合理初始化,如下图所示:

 

如果直接创建对象并且使用有可能造成各种不理想结果,甚至是异常故障。

可以用抽象父类来解决这个问题。

3.1.2 抽象类示例

为便于理解抽象类的作用,我们先开发一个不使用抽象类的案例,查看此时可能存在的问题。

/**
 * Person类的作用是为子类提供代码复用
 */
public class Person {
    String name;
    int age;
    public void whoru() {
        System.out.println("我是"+name);
    }
}
package day07.abstract01;
public class Student extends Person{
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void study() {
        System.out.println("学习");
    }
}
package day07.abstract01;
public class Teacher extends Person{
    public Teacher(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void teach() {
        System.out.println("讲课");
    }
}
package day07.abstract01;
public class Worker extends Person{
    public Worker(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void work() {
        System.out.println("工作");
    }
}
package day07.abstract01;
public class AbstractDemo1 {
    public static void main(String[] args) {
        Student s = new Student("Tom", 12);
        Teacher t = new Teacher("Andy", 28);
        Worker w = new Worker("Jerry", 28);
        s.whoru();
        s.study();
        t.whoru();
        t.teach();
        w.whoru();
        w.work();
        //问题:如果能够直接创建Person对象,其方法运算结果不理想
        Person p = new Person();
        p.whoru();
    }
}

上面案例的运行结果如下所示:

 

上述运行结果中创建Person类型对象,调用其whoru()方法,得到结果是null。

这个结果并不理想,就像生活中问一个人是谁,回答“佚名”一样。造成这个结果的原因是:泛化出来的父类Person 是个半成品类,创建其对象后,其name属性没有合理初始化,导致输出了name的默认值 null。

如何解决上述问题呢?需要明确表示 Person是一个半成品类,不能直接实例化。

因此,就可以使用abstract修饰一下Person类型,使其成为抽象类,这样Java的编译器就会限制Person类型,不允许其直接实例化创建对象。不能被实例化,就不可能造成后续结果了。所以合理使用abstract类,可以避免创建不应该创建的对象,减少程序的错误。

将上述案例中的Person类声明为抽象类,可避免出现创建半成品实例的情况。案例代码示意如下:

/**
 * Person类的作用是为子类提供代码复用
 */
public abstract class Person {
    String name;
    int age;
    public void whoru() {
        System.out.println("我是"+name);
    }
}
package day07.abstract02;
public class AbstractDemo2 {
    public static void main(String[] args) {
        Student s = new Student("Tom", 12);
        Teacher t = new Teacher("Andy", 28);
        Worker w = new Worker("Jerry", 28);
        s.whoru();
        s.study();
        t.whoru();
        t.teach();
        w.whoru();
        w.work();
        //问题:如果能够直接创建Person对象,其方法运算结果不理想
        //Java编译器检查,不允许创建抽象类型的对象!
        //Person p = new Person();
        //p.whoru();
    }
}

3.1.3 抽象类不可以实例化

面向对象设计时候根据子类泛化得到的半成品父类,应该定义为抽象类,这样可以限制创建半成品类的对象,减少意外的错误发生。

正因为抽象类不可以被实例化,因此 abstract 和 final 不可以同时修饰一个类:final 关键字使得类不可被继承,而抽象类既不能能被继承,又不能被实例化,则没有任何意义。

使用抽象类时:

1、在类名前面添加abstract关键字以后就是抽象类了

2、抽象类可以作为父类被子类继承,可以定义变量

3、抽象类不能直接创建对象

3.2 抽象方法

3.2.1 什么是抽象方法

使用abstract关键字声明,不包含方法体的方法称为抽象方法。

这个定义同样晦涩,究其原因是因为在利用泛化设计父类时候,有这种情况全体子类都有相同的方法,但是每个具体方法实现都并不相同,这样只能将方法名抽取到父类,方法体留在每个子类中,这种只有方法名称的方法,就是抽象方法。

3.2.2 抽象方法的语法

抽象方法的语法:

1、使用abstract声明方法,不能有方法体

2、包含抽象方法的类必须声明为抽象类,因为包含抽象方法的类一定是不完整的半成品类

3、子类继承抽象类时候必须重写(实现)抽象方法,否则出现编译错误

  • 可以将抽象方法看作父类对子类的行为约定,必须被子类重写实现

实际使用时,如何选择抽象方法?建议规则:每个子类都有,但是每个子类实现都不同的方法泛化为抽象方法!

3.2.3【案例】抽象方法示例

定义抽象类,并包含抽象方法;定义子类继承自抽象类,并重写抽象方法。编写代码测试其特点。

案例示意代码如下:

/**
 * Person类的作用是为子类提供代码复用
 * 设计为抽象类,只能被继承,不能创建对象
 */
public abstract class Person {
    String name;
    int age;
    public void whoru() {
        System.out.println("我是"+name);
    }
    /**
     * 日程计划
     */
    public abstract void schedule();
}
package day07.abstract03;
public class Student extends Person {
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void study() {
        System.out.println("学习");
    }
    public void schedule() {
        System.out.println("吃饭、听课");
    }
}
package day07.abstract03;
public class Student extends Person {
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void study() {
        System.out.println("学习");
    }
    public void schedule() {
        System.out.println("吃饭、听课");
    }
}
package day07.abstract03;
public class Worker extends Person {
    public Worker(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void work() {
        System.out.println("工作");
    }
    public void schedule() {
        System.out.println("吃饭、开车");
    }
}
package day07.abstract03;
public class AbstractDemo3 {
    public static void main(String[] args) {
        Student s = new Student("Tom", 12);
        Teacher t = new Teacher("Andy", 28);
        Worker w = new Worker("Jerry", 28);
        s.schedule();
        t.schedule();
        w.schedule();
    }
}

3.2.4 抽象类的意义

综合抽象类和抽象方法的讲解,可以简单理解抽象类的意义如下:

  • 为其子类提供一个公共的类型
  • 封装子类中的重复内容(成员变量和方法)
  • 定义有抽象方法,子类虽然有不同的实现,但该方法的定义是一致的
  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值