Java基础10—类的高级特性

类的高级特性


参考资料:《Java从入门到精通》/明日科技编著. 4版. 北京:清华大学出版社,2016

一、Java类包

在Java中每定义一个类,通过Java编译器进行编译后,都会生成一个扩展名为.class的文件,当程序的规模逐渐庞大时,就很容易产生类名称冲突的现象。在Java中提供了一种管理类文件的机制,就是类包

1、类名冲突

Java中每个接口或类都来自不同的类包,如果没有包的存在,管理程序中的类名称将变得非常麻烦。假如,在程序中已经定义一个Login类,但因为业务需求,还要定义一个名为Login类。这时问题就来了,编译器不允许存在同名的类文件。解决问题的办法就是将这两个类文件放置在不同的类包中。

2、完整的类路径

Math类的完整路径如下所示:

java.lang.Math;
  • java.lang:包的名称
  • Math:类的名称

由上述可知,一个完整的类名需要包名和类名的组合,每个类都隶属于一个包,只要保证同一包中的类不同名,就可以有效避免同类名冲突的情况。

如果一个程序中同时要用到java.util.Date和java.sql.Date类,如果在程序中不指定完整的类路径,编译器就不知道该使用java.util类包中的Date类还是java.sql类包的Date类。

java.util.Date date1 = new java.util.Date();
java.sql.Date date2 = new java.sql.Date();

说明:在同一个包名中的类可以相互访问,可以不指定包名。

注意:同一个包中的类可以不放在同一个位置,如com.xuwenjia.Hello1和com.xuwenjia.Hello2可以一个在C盘,一个在D盘,只要将CLASSPATH分别指向这两个位置即可。

3、创建包

在Java中,包名的设计应与文件系统结构相对应,如果一个包名为com.lwx,那么该包的类都位于com文件夹下的lwx文件夹内。在类中定义包名的语法如下所示:

package 包名

在程序中指定包名时,必须将package表达式放置在程序的第一行,在使用位于com.lwx包下的Dog.java类时,必须指定类的全名,如下所示:

package com.lwx.Dog

注意:Java包的命名规则是全部使用小写。

除了类名冲突,众多的包也会产生包名冲突。为了避免包名冲突,在Java中定义包名时,通常采用创建者的Internet域名的反序,由于Ineternet域名是独一无二的,包名自然不会发生冲突。

//指定包名
package com.demo5;

public class Math {
    public static void main(String[] args) {
        System.out.println("不是java.lang.Math类,而是com.demo5.Math类");
    }
}

在上述代码中,在程序的第一行指定包名,同时在包内定义一个Math类,注意这里的Math类不是Java.lang.Math类,而是com.demo5.Math类。可以看出不同的包中可以定义相同的类名。

4、导入包

(1)使用import导入包

如果一个类中需要使用Math类,那么该如何告诉编译器是使用Java.lang.Math类还是com.demo5.Math类呢?此时,需要使用import关键字进行指定。例如,如果在程序中使用 import com.demo5 表达式,在程序中使用Math类的时候就会选择com.demo5.Math类来使用。

import关键字的语法格式为:

import com.lwx.*;   //指定com.lwx包中所有的类在程序中都可以使用
import com.lwx.Math    //指定com.lwx包中的Math类可以在程序中使用

注意:如果类中已经导入com.demo5.Math类,那么在类体中使用其他包中的Math类时则必须指定完整的带有包格式的类名,如这种情况再使用java.lang包的Math类就要使用全面格式Java.lang.Math。

(2)使用import导入静态成员

import关键字还可以导入静态成员,使得编程更为方便。使用import导入静态成员的语法格式如下所示:

import static 静态成员

下面具体演示如何使用import导入静态成员:

package com.demo5;

import static java.lang.Math.max;    //导入静态成员方法
import static java.lang.Math.PI;     //导入静态成员变量

public class ImportTest {
    public static void main(String[] args) {
        //在主方法中直接调用这些静态成员
        System.out.println("2和5的较大值为:" + max(2,5));
        System.out.println(PI);
    }
}

输出结果:
2和5的较大值为:5
3.141592653589793

二、final变量

final关键字可用于变量声明,一旦变量被设定为final,就不可以改变该变量的值。通常,由final定义的变量为常量。例如,在类中定义PI的值,如下所示:

final double PI = 3.1415926;
  • 当程序使用PI这个常量时,它的值就是3.1415926,如果在程序中再次对PI常量进行赋值,则编译不通过。
  • final关键字定义的变量必须在声明的同时进行初始化赋值;
  • final关键字除了修饰基本数据类型的常量,还可以修饰对象引用;
  • 数组也可以看作一个对象来引用,所以final可以修饰数组;
  • 一旦对象引用被修饰为final以后,它只能恒定指向一个对象,无法将其改变指向另外一个对象。
  • 一个既是static又是final的字段只占据一段不能改变的存储空间。
package com.demo5;

import java.util.Random;

class Test {
    int i = 12;
}

public class FinalTest {

    static Random random = new Random();
    private final int VALUE_1 = 9;          //声明一个final常量
    private static final int VALUE_2 = 10;  //声明一个static、final常量
    private final Test test = new Test();   //声明一个final型对象引用,该引用指向一个Test类型对象
    private Test test2 = new Test();        //声明一个非final型对象引用,该引用指向一个Test类型对象
    private final int[] a = {1,2,3,4,5};    //声明一个final型数组
    private static final int b = random.nextInt(20);

    public String toString(){
        return a + " " + b;
    }

    public static void main(String[] args) {
        FinalTest finalTest = new FinalTest();
        System.out.println(finalTest.VALUE_1);   //调用常量VALUE_1
        System.out.println(FinalTest.VALUE_2);   //调用静态常量VALUE_2
        System.out.println(finalTest.test.i);
        System.out.println(finalTest.test2.i);
        //finalTest.test = new Test();    //该语句错误,常量不能第二次赋值
        finalTest.test2 = new Test();

        for (int i = 0; i < finalTest.a.length; i++) {
            System.out.print(finalTest.a[i] + " ");
        }
        System.out.println();

        System.out.println(FinalTest.b);
    }
}

输出结果:
9
10
12
12
1 2 3 4 5 
6
  • 在Java的编码规范中,被定义为final常量需使用大写字母命名,并且使用下划线进行连接;
  • 定义为final的数据,无论是常量、对象引用还是数组,在主函数中都不可以改变。

之前提到过,被定义为final的对象引用只能指向唯一的对象,不可以再将它指向其他对象,但是一个对象本身的值是可以改变的,那么为了是一个常量做到真正不可修改,可以将常量声明为static final。

package com.demo_5_13;

import java.util.Random;

public class FinalStaticTest {
    //实例化一个Random类对象
    private static Random random = new Random();
    //产生一个0~10的随机数赋予常量a1
    private final int a1 = random.nextInt(10);
    //产生一个0~10的随机数赋予静态常量a2
    private static final int a2 = random.nextInt(10);

    public static void main(String[] args) {
        FinalStaticTest f1 = new FinalStaticTest();
        System.out.println(f1.a1);
        System.out.println(f1.a2);
        
        System.out.println("------------------------");
        
        FinalStaticTest f2 = new FinalStaticTest();
        System.out.println(f2.a1);
        System.out.println(f2.a2);
        
        System.out.println("------------------------");
        
        FinalStaticTest f3 = new FinalStaticTest();
        System.out.println(f3.a1);
        System.out.println(f3.a2);
        
    }
}

输出结果:
6
6
------------------------
4
6
------------------------
3
6

从上述代码执行结果来看,定义为final的常量的值不是恒定不变的,将随机数赋予给定义为final的a1,可以做到每次允许程序时改变a1的值。但是a2与a1不同,由于它被声明为static final形式,所以在内存中为a2开辟了一个恒定不变的区域。每当实例化一个FinalStaticTest对象时,仍然指向a2这块区域,所以a2的值保持不变。a2在装载时被初始化,而不是每次创建对象时被初始化,而a1会在重新实例化对象时被更改。

技巧:在Java中定义全局常量,通常使用public static final进行修饰,这样的常量只能在定义时被赋值,且不被改变。

最后总结一下,在程序中final数据可以出现的位置:

package com.demo_5_13;

public class FinalData {
    final int VALUE_1 = 6;   //final成员变量不可更改
    final int VALUE_2;       //在声明final成员变量时没有赋初值,称为空白final。这种情况必须在构造方法中赋值,否编译不通过
    
    public FinalData(){
        VALUE_2 = 10;    //在构造方法中为空白final赋初值
    }
    
    public int add(final int a){   //设置final参数,不可以改变参数a的值
        return a + 1;
    }
    
    public void speak(){    
        final int i = 4;    //局部变量定义为final,方法体中不可以改变i的值
    }
}

三、final方法

  • 首先,定义为final的方法无法被重写
  • 将方法定义为final类型可以防止子类修改该类的定义与实现方式,同时定义为final的方法的执行效率要高于非final方法。
  • 如果一个父类的某个方法被设置成private修饰符,子类将无法访问该类,自然无法覆盖该方法。
  • 所以定义为private的方法已经隐式定义为final类型。
private void test(){
    //.....
}

等价于:
private final void test(){
    //.....
}

但是,在父类中被定义为private final的方法似乎可以被子类覆盖,如下所示:

package com.demo_5_14;

class Parents{
    private final void go1(){
        System.out.println("go111");
    }
    final void go2(){
        System.out.println("go222");
    }
    public void go3(){
        System.out.println("go333");
    }
}

class Child extends Parents{
    public final void go1(){
        System.out.println("child.go111");
    }
//    public final void go2(){                 //无法覆盖final方法
//        System.out.println("child.go222");
//    }
    public void go3(){
        System.out.println("child.go333");
    }
    public void go4(){
        System.out.println("child.go444");
    }
}

public class FinalMethod {
    public static void main(String[] args) {
        Child child = new Child();     //实例化一个子类对象
        child.go1();
        child.go2();      //调用父类的go2()方法
        child.go3();
        child.go4();

        System.out.println("-------------");

        Parents parents = new Child();    //执行向上转型操作
//        parents.go1();     //无法调用private方法
        parents.go2();
        parents.go3();       //父类拥有go3()方法,但实际调用子类的go3()方法
//        parents.go4();     //向上转型操作后,无法调用父类没有定义的方法

        System.out.println("-------------");

        Parents parents1 = new Parents();     //实例化一个父类对象
//        parents1.go1();     //无法调用private方法
        parents1.go2();
        parents1.go3();
    }
}
输出结果:
child.go111
go222
child.go333
child.go444
-------------
go222
child.go333
-------------
go222
go333

从上述代码可以看出,final方法不能被覆盖,例如go2()方法无法在子类中被重写。但是在父类中定义了一个修饰为private final的go1()方法,同时在子类中也定义了一个go1()方法,从表面上看,子类中了go1()方法覆盖了父类的go1()方法,但是覆盖必须满足一个对象向上转型为它的基本类型并调用相同方法这样一个条件。例如,“Parents parents = new Child(); ”语句执行向上转型操作,对象parents只能调用正常覆盖的go3()方法,却不能调用go1()方法,可见子类中的go1()方法并不是正常覆盖,而是生成了一个新的方法。

四、final类

定义为final的类无法被继承

如果希望一个类不允许任何类继承,并且不允许其他人对这个类进行任何改动,可以将这个类设置为final形式。final形式如下所示:

final 类名 {
    //....
}

如果将一个类设置为final形式,则类中的所有方法都被隐式设置为final形式,但是final类中的成员变量可以被定义为final或非final形式。

final class FinalClass {
    int a = 10;          //非final型成员变量
    final int b = 20;    //final型成员变量
    void go(){           //final型成员方法
        System.out.println("gogo");
    }

    public static void main(String[] args) {
        FinalClass f1 = new FinalClass();
        f1.a++;    //可以改变a的值
        // f1.b++;     //编译报错,无法改变b的值
        System.out.println(f1.a);
        
        f1.go();
    }
}

输出结果:
11
gogo

五、内部类

前面内容学过,类与类之间是相互独立的,任何一个类都不在另一个类的内部,而如果在一个类中再定义一个类,则该定义的类称为内部类。内部类可以分为成员内部类、局部内部类以及匿名类。

1、成员内部类

(1)成员内部类简介

在一个类中使用内部类,可以在内部类中直接存取其所在类的私有成员变量。首先介绍成员内部类,成员内部类的语法如下所示:

public class OuterClass {       //外部类
    private class InnerClass {  //内部类
        //...
    }
}

在内部类中,可以随意使用外部类的成员变量和成员方法,尽管这些类成员被修饰为private。

内部类的实例化操作一定要绑定在外部类的实例上,如果从外部类中初始化一个内部类对象,那么内部类对象就会绑定在外部类对象上。内部类的实例化方式与其他类实例化方式相同,都是使用new关键字。

public class OuterClass {
    //实例化一个内部类对象
    InnerClass innerClass = new InnerClass();
    //创建外部类的成员方法
    public void outf(){
        innerClass.inf();    //调用内部类对象的inf()方法
    }
    //创建外部类的成员方法,返回值类型为内部类对象引用
    public InnerClass doit(){
        // y = 10;   //外部类不可以直接调用内部类的成员变量,必须使用下条代码的形式
        innerClass.y = 10;
        return new InnerClass();    //返回内部类对象引用
    }
    
/*    
*定义一个内部类
*/
    class InnerClass{
        //内部类的构造方法
        InnerClass(){

        }
        //定义一个内部类成员方法
        public void inf(){
            System.out.println("这是一个内部类的方法");
        }
        //定义一个内部类成员变量
        int y = 0;
    }

    public static void main(String[] args) {
        //创建外部类对象
        OuterClass out = new OuterClass();
        //内部类对象的实例化操作必须在外部类或外部类的非静态方法中实现
        //OuterClass.InnerClass in2 = new OuterClass.InnerClass();      //编译不通过
        OuterClass.InnerClass in = out.new InnerClass();    //内部对象的实例化操作,即外部类创建内部类实例的方式
        //在主方法中实例化内部类对象,必须在new操作符之前提供一个外部类的引用
        out.outf();   //调用外部对象的成员方法
        in.inf();    //调用内部对象的成员方法
    }
}

输出结果:
这是一个内部类的方法
这是一个内部类的方法

在上述代码中,外部类创建内部类实例与其他类创建对象引用时相同。内部类可以随意访问外部类的类成员,内部类的成员只有在内部类的范围内是可知的,不能被外部类使用。但是外部类可以通过创建内部类的对象才能调用内部类对象的类成员。

public class OuterClass {
    private int a = 1;       //修饰为private的成员变量
    private void run(){      //修饰为private的成员方法
        
    }
    //内部类
    class InnerClass { 
        void inf(){
            run();    //调用修饰为private的run()方法
            a++;      //调用修饰为private的a成员变量
        }
    }
}

在主方法内实例化内部对象,必须在new操作符之前提供一个外部类的对象引用。

    public static void main(String[] args) {
        OuterClass out = new OuterClass();           //创建外部类对象
        OuterClass.InnerClass in = out.new InnerClass();    //内部对象的实例化操作,使用外部类的对象来创建
        //OuterClass.InnerClass in2 = new OuterClass.InnerClass();   //错误代码

从上述代码可以看出,在实例化内部对象时,不能在new操作符之前使用外部类的类名来实例化内部类对象,而是应该使用外部类的对象来创建内部类的对象。

注意:内部类对象会依赖于外部类对象,必须先创建外部类对象,然后才能创建内部类对象。

(2)内部类向上转型为接口

如果将一个权限修饰符为private的内部类向上转型为其父类对象,或者直接向上转型为一个接口,在程序中就可以完全隐藏内部类的具体实现过程。

可以在外部提供一个接口,在接口中声明一个方法。如果在实现该接口的内部类中实现该接口的方法,就可以定义多个内部类以不同的方式实现接口中的同一个方法,而在一般的类中是不可能多次实现接口中的同一个方法的。

package com.demo_5_14;
/*
*定义一个接口
*/
interface OutInterface {
    public void f();
}
/*
 *定义一个外部类
 */
class OuterClass2 {
    /*
    *定义一个内部类1,并实现OutInterface接口
    */
    private class InnerClass1 implements OutInterface{
        //创建内部类1的构造方法
        public InnerClass1(String s){
            System.out.println(s);
        }
        //实现接口的方法
        @Override
        public void f() {
            System.out.println("接口f()方法的第1种实现方法");
        }
    }
    /*
    *定义一个内部类2,并实现OutInterface接口
    */
    private class InnerClass2 implements OutInterface {
        //创建内部类2的构造方法
        public InnerClass2(String s){
            System.out.println(s);
        }
        //实现接口的方法
        @Override
        public void f() {
            System.out.println("接口f()方法的第2种实现方式");
        }
    }
    //定义一个方法1,返回一个内部类1的实例化对象,返回值类型为OutInterface接口
    public OutInterface doit1(){
        return new InnerClass1("调用内部类1的构造方法");
    }
    //定义一个方法2,返回一个内部类2的实例化对象,返回值类型为OutInterface接口
    public OutInterface doit2(){
        return new InnerClass2("调用内部类2的构造方法");
    }

}

public class InterfaceInner {
    public static void main(String[] args) {
        //实例化一个OuterClass2类型对象
        OuterClass2 out = new OuterClass2();

        //调用doit1()方法,并向上转型为接口类型
        OutInterface outInterface1 = out.doit1();
        //调用接口的f()方法
        outInterface1.f();   //该语句的执行结果为内部类f()方法的执行结果

        System.out.println("-----------");    //分割线

        //调用doit2()方法,并向上转型为接口类型
        OutInterface outInterface2 = out.doit2();
        //调用接口的f()方法
        outInterface2.f();  //该语句的执行结果为内部类f()方法的执行结果

    }
}

输出结果:
调用内部类1的构造方法
接口f()方法的第1种实现方法
-----------
调用内部类2的构造方法
接口f()方法的第2种实现方式

从上述代码中可以看出,OuterClass2类中定义了一个修饰符为private的内部类InnerClass,这个内部类实现了OutInterface接口,然后修改doit()方法,使得该方法返回一个OutInterface接口。由于内部类InnerClass的修饰权限为private,所以除了OuterClass2类可以访问该内部类以外,其他类都不能访问,但是可以访问doit()方法。由于该方法返回一个外部接口类型,这个接口可以作为外部使用的接口。它包含一个f()方法,在继承此接口的内部类中实现了该方法,如果某个类继承了OuterClass2外部类,由于内部的权限不可以向下转型为内部类InnerClass,同时也不能访问f()方法,但是却可以访问接口中的f()方法。例如,上述代码的最后一条语句,接口引用调用f()方法,从执行结果来看,该语句执行的就是内部类的f()方法,很好的对继承该类的子类隐藏了实现细节,仅仅为编写子类的人留下了一个接口和一个外部类,同时也可以调用f()方法,但是f()方法的具体实现细节却被很好的隐藏了,这就是内部类的最基本用途。

注意:非内部类不能被声明为private或protected类型。

(3)使用this关键字获取内部类与外部类的引用

如果在外部类定义的成员变量与内部类定义的成员变量名称相同,可以使用this关键字。

package com.demo_5_14;

public class OuterClass3 {
    private int x;
    private class InnerClass{
        private int x;
        public void doit(int x){
            x++;                   //调用形参x
            this.x++;              //调用内部类成员变量x
            OuterClass3.this.x++;  //调用外部类的成员变量x
        }
    }
}

应该明确一点,在内存中所以的对象均被放置在堆中,方法以及方法中的形参或局部变量放置在栈中。

2、局部内部类

内部类不仅可以在类中进行定义,也可以在类的局部位置进行定义,例如在类的方法或任意的作用域均可以定义内部类。

package com.demo_5_14;

interface OutInterface2 {

}

public class OuterClass4 {
    private int a = 1;
    //定义一个成员方法,返回接口类型对象引用
    public OutInterface2 doit(final String x){
        //定义内部类,并实现接口
        class InnerClass implements OutInterface2{
            //内部类构造方法
            InnerClass(String x){
                System.out.println(x);
            }
            //内部类成员方法,调用外部类对象的成员变量a
            public void doit(){
                OuterClass4.this.a = 2;
            }
        }
        //返回一个内部类对象
        return new InnerClass("doit");
    }
    
    /*  public void doit2(){
        InnerClass innerClass = new InnerClass("doit2");   //外部类的其他方法不能访问内部类
    }*/
}

从上述代码可知,内部类被定义在doit()方法内部。但是值得注意的是,内部类InnerClass是doit()方法的一部分,并非外部类OuterClass4的一部分,所以在doit()方法的外部不能访问内部类InnerClass,但是该内部类可以访问当前代码块的常量以及此外部类的所有类成员。

这里有一处细节,就是将doit()方法的参数设置为final类型,这是因为在方法中定义的内部类只能访问方法中的final类型的局部变量。该局部变量设置为final类型后,在内部类中无法改变该局部变量的值。

3、匿名内部类

首先看下面的代码:

//定义接口
interface OutInterface {
    
}

public class OuterClass {
    //定义doit()方法
    public OutInterface doit(){
        return new OutInterface()
            {                      
            private int a = 1;       //匿名内部类体
            public int getValue(){   //匿名内部类体
                return a;            //匿名内部类体
            }
        };    //注意这里的分号
    }
}

上述的代码有点莫名奇妙,但是这种写法确实被Java编译器认可。在doit()方法内部先返回一个OutInterface的引用,然后在reture语句中插入一个定义内部类的代码,由于这个类没有名称,所以将该内部类称为匿名内部类。实质上这种内部类的作用就是创建一个实现于OutInterface接口的匿名类的对象。

匿名内部类的所有代码都需要在花括号内编写,语法如下:

return new A() {
    //...内部类体
};   //注意分号

由于匿名内部类没有名称,所以匿名内部类使用默认构造方法生成OutInterface对象。在匿名内部类定义结束后,需要加分号,这个分号不是代表定义内部类结束的标识,而是return语句结束的标识。

说明:匿名内部类编译以后,会产生以“外部类名$序号”为名称的.class文件,序号以1~n排序,分别代表1~n个匿名类。

4、静态内部类

  • 在内部类前面添加修饰符static,这个内部类就成为静态内部类了。
  • 一个静态内部类中可以声明static成员,但是在非静态内部类中不可以声明静态成员。
  • 静态内部类有一个最大的特点,就是不可以使用外部类的非静态成员,所以静态内部类在程序开发中比较少见。

静态内部类的两个特点:

  1. 如果创建静态内部类的对象,不需要外部类的对象;
  2. 不能从静态内部类的对象中访问非静态外部类的对象。
package com.demo_5_16;

public class StaticInnerClass {
    private int sum = 1;
    //声明静态内部类
    static class inner {
        void doit(){
            // System.out.println(sum);  //无法调用外部类成员变量sum
        }
    }
}

在上述代码中,在静态内部类的doit()方法中调用外部类成员变量sum,但是成员变量sum并非静态类型,所以无法调用sum变量。

5、内部类的继承

内部类和其他类一样可以被继承,但是继承内部类比继承普通类更复杂,需要设置专门的语法。

package com.demo_5_16;

class ClassA {
    class ClassB {
        //....
    }
}

public class OutputInnerClass extends ClassA.ClassB{
    public OutputInnerClass(ClassA a){
        a.super();
    }
}

在某个类继承内部类时,必须硬性给予这个类一个带参数的构造方法,并且该构造方法的参数为需要继承内部类的外部类的引用,同时在构造方法体中使用a.super()语句,这样才为继承提供了必要的对象引用。

posted @ 2020-05-16 16:12  黑色幽默2020  阅读(233)  评论(0编辑  收藏  举报