Java中的陌生事物:常量


介绍

在本文中,我们将探讨一些涉及使用常量的场景,即使是经验丰富的程序员也可能对此有所怀疑。尽管该主题可能是众所周知的,但并不是每个人都探索了特定的场景,例如在同义常量存在的情况下解决多重继承。增强自己的理论基础对于能够自信地进行编程至关重要。

定义

一个常量是一个变量,其价值一旦被分配不能改变。这意味着,如果程序中的一条指令试图更改常量的值,我们将得到一个编译时错误。final修饰符将Java中的变量转换为常量。它将可能分配给一个变量的数量限制为一个。例如,如果我们写:

final int CONST = 0;
CONST = 1;

我们得到以下编译错误:

error: cannot assign a value to final variable CONST
       CONST = 1;
       ^
1 error

这就是为什么常量的命名约定要求您仅使用大写字母(_如果名称由多个单词组成的情况,则使用下划线字符“ ”)。实际上,这种特殊的约定保证了我们能够立即区分常量和变量。这将使更难以像前一个一样犯错误。

持续分类

当我们习惯于将变量分为实例变量,类变量和局部变量时,我们还必须区分类常量,实例常量和局部常量。特别是,在前面的代码段中,我们定义了局部常量。

还要考虑局部常量,甚至是某些特殊情况,例如方法的常量参数(请参见下面的专用部分),或在某些编程结构的声明的上下文中定义的常量。例如,我们可以通过将临时变量声明为常量来编写一个foreach循环,如下所示:

for (final var tmp : arr) {
    System.out.println(tmp);
}

一些开发人员发现这种做法有助于强调在构造的代码块内不得更改临时变量。请注意,能够将变量声明tmp为常量,使我们理解此变量实际上是局部的,而不是相对于循环构造而言,而是相对于循环的当前迭代而言。如果不是这种情况,则final修饰符将阻止重新分配值(因此无法编译前一个循环)。

这种行为不同于我们通常在for循环中初始化的变量的行为,该变量具有循环级作用域而不是迭代级作用域。实际上,以下代码段:

for (final int i = 0; i < 10; i++) {
    //code omitted
}

会产生错误:

error: cannot assign a value to final variable i
        for (final int i = 0; i < 10; i++) {
                                      ^
1 error

观察之后,让我们继续看一些更有趣的场景。

局部常数

如今,局部常量的使用相对较少,但并非总是如此。实际上,还有一种编程风格,要求final对每个局部变量使用修饰符,每个局部变量在其生命周期中都被分配了该修饰符。实际上,通过声明final一个变量的值将不会被修改,它将立即清楚地定义其定义的逻辑:不得更改其值。这是在我们的代码中施加约束的一种方式,将来也会对其进行修改的其他程序员也必须遵守这些约束。这种编程风格与面向对象设计的原理一致,并且被某些人认为是最佳实践。例如,当使用诸如Eclipse之类的IDE时,执行“提取局部变量”重构以将方法的返回值分配给自动创建的变量,默认情况下,final修饰符也会添加到该变量。

1613212628537.png

图1:“提取局部变量”时添加final的Eclipse对话框

但是,Java冗长而闻名。因此,final如今很少使用将修饰符添加到不改变其值的变量的趋势。通过在版本8中引入有效的final变量的概念,Oracle本身一直在努力减少Java的冗长冗长的工作,这也清楚地表明:程序员喜欢不太冗长的样式。实际上,在Java 8使用局部嵌套类(以及随后在lambda表达式中)中使用局部变量之前,必须对其进行声明,而现在仅将变量初始化一次就足够了。

常数参数

方法的参数也可以声明final。例如,我们可以声明mainmethod参数final以防止重新分配。例如:

public static void main(final String args[]) {
    args = new String[5];
    //rest of code omitted...

将产生以下编译时错误:

error: final parameter args may not be assigned
        args = new String[5];
        ^
1 error

即使在方法参数的情况下,final通常也避免使用,但是在某些情况下它可能是有用的。原因始终是相同的,以使代码更具表现力,同时又增加了冗长性。

实例常量

但是大多数时候,我们使用常量作为类的字段。特别是,我们通常在上下文中根据其声明初始化常量,例如,通过以下方式:

public class FileManager {
    private final static String FILE_NAME = "aFile.java";
    //rest of code ommitted...
}

请注意,在这种情况下,我们也已声明了常量static,这可以被视为最佳实践。实际上,通过这种方式,static修饰符将确保该类的常量存在单个副本,该副本将由所有实例化对象共享。这将防止在运行时为每个实例化对象在内存中创建常量的相同副本。实际上,常数具有固定值,因此同一类的所有对象都共享该常数是没有风险的,因为其值不能更改。常量声明final和static被称为类常量。

显然,如果我们希望每个实例化的对象的常数都具有不同的值,则static不应使用修饰符。

实例常量

我们说过,通常我们会在声明常量的同时初始化它。但这不是强制性的,实际上也可以在构造函数中初始化实例常量。以下面的类为例:

public class FileManager {
    private final String FILE_NAME;

    public FileManager(String fileName) {
        FILE_NAME = fileName;
    }
}

有一个构造函数,该构造函数FILE_NAME通过来自外部的参数将值分配给常量。这使您可以FILE_NAME为每个对象的常数分配不同的值。例如,我们可以这样写:

FileManager readmeFM = new FileManager("readme.txt");
FileManager licenseFM = new FileManager("license.txt");

这样,两个对象将具有FILE_NAME不同的初始化常量。

显然,如果我们宣布恒FILE_NAME也static,这将不会成为可能。请注意,在这种情况下,编译器会给出误导性的错误消息:

error: cannot assign a value to final variable FILE_NAME
        FILE_NAME = fileName;
        ^
1 error

实际上,似乎表明问题出在final修饰符上,但实际上问题出在static修饰符的使用上。

实例常量和方法

请注意,在不是构造函数的方法中,不可能设置实例常量的值。例如,如果我们写了:

public class FileManager {
    private final String FILE_NAME;
    public FileManager(String fileName) {
        setFILE_NAME(fileName);
    }
    public void setFILE_NAME(String fileName) {
        FILE_NAME = fileName;
    }
}

我们将收到以下编译错误:

error: cannot assign a value to final variable FILE_NAME
        FILE_NAME = fileName;
        ^
1 error

这是因为,与构造函数不同,该setFILE_NAME方法在运行时可能被多次调用,从而导致违反了由final修饰符定义的约定。

实例常量和构造函数重载

还要注意,在构造函数重载的情况下,编译器将能够识别常量是否正确初始化。例如,如果我们将无指令构造函数添加到FileManager类中,如下所示:

public class FileManager {
    private final String FILE_NAME;
    public FileManager() {
    }
    public FileManager(String fileName) {
        FILE_NAME = fileName;
    }
}

编译器将理解,如果我们调用第二个构造函数,则该常量FILE_NAME将不会初始化,因此将向我们显示此错误消息:

error: variable FILE_NAME might not have been initialized
    }     
    ^
1 error

另一方面,如果使用第一个构造函数,我们使用this关键字调用第二个构造函数,并为其传递默认文件的名称,则程序将正确编译:

public class FileManager {
    private final String FILE_NAME;
    public FileManager() {
        this("defaultFile.txt");
    }
    public FileManager(String fileName) {
        FILE_NAME = fileName;
    }
}

实际上,不可能进行FILE_NAME多次设置。

常量和封装

可能还会声明我们的实例或类常量public。实际上,即使我们习惯于封装变量以防止它们采用不想要的值,也没有必要封装我们的常量,因为它们永远都不可能采用不想要的值。

在标准的Java库,我们可以找到很多static和public常数,如PI和E中的常量Math类,或者MAX_PRIORITY,NORM_PRIORITY和MIN_PRIORITY该的Thread类。

常数,多态性和继承

对于多态性规则,我们知道,如果使用超类的引用调用对象的方法,则将调用实例化该对象的类的重写方法,而不是引用类的方法。使用。 实际上,如果考虑以下简单的类层次结构:

abstract class Pet {
    public final String type = "Generic Pet";
    public abstract void talk();
}
class Dog extends Pet {
    public final String type = "Dog"; 
    public void talk() {
        System.out.println("Woof woof!")
    }
}

包含以下代码段:

Pet bobby = new Dog();
bobby.talk();

我们将调用talk在Dog类中重新定义的方法,而不是类的原始方法Pet(顺便提一下,它是抽象的)。

如果我们试图访问公共变量或常量(包括形势的变化static和非static),这是我们在子类覆盖。实际上,类的属性没有覆盖,因此规则是不同的。例如,以下代码:

Pet snoopy = new Dog();
System.out.println(snoopy.name);
Dog punky = new Dog();
System.out.println(punky.name);

将产生以下输出:

Generic Pet
Dog

这意味着即使常量名已在Dog子类中重写,要访问它,我们也需要相同类的引用。实际上,使用Pet超类的引用Pet可以访问该类的常量。

常量和多重继承

从Java版本8开始,通过在接口中引入默认方法,可以使用一种新的多重继承。这与某些语言(例如C ++)中定义的复杂功能不同,而是接口概念演变的简单结果。管理Java的多重继承的规则非常简单,唯一可以引起疑问的规则称为“类总是赢”。实际上,如果我们从一个类和一个接口继承具有相同签名的两个方法,则该类中的一个将始终被继承(该类总是获胜)。在所有其他继承方法都相同的情况下,编译器会强制我们覆盖该方法。

也就是说,我们知道接口无法声明变量,但是它们可以声明静态和公共常量。实际上,甚至没有义务用public,static和final修饰符标记它们,它们在接口中是隐式的。那么,如果我们从两个不同的类型继承具有相同名称的常量,它将如何工作?答案是编译器将始终迫使我们重写常量。例如。让我们考虑以下代码:

abstract class AbstractClass {
    public static final int VALUE = 1;
}
interface Interface {
    int VALUE = 2;
}
class Subclass extends AbstractClass implements Interface {
    public static void main(String args[]) {
        System.out.println(VALUE);
    }
}

如果我们尝试编译包含先前代码的文件,则会收到以下编译错误:

error: reference to VALUE is ambiguous
        System.out.println(VALUE);
                           ^
  both variable VALUE in AbstractClass and variable VALUE in Interface match
1 error

因此,对于常量而言,“类别始终获胜”规则不适用。特别是在重写常量的过程中,必须始终引用其所属的类,例如:

System.out.println(AbstractClass.VALUE);

结论

在本文中,我们探讨了常量使用的某些方面,常量是该语言的基本主题,有时会肤浅使用。相反,我们已经看到,在某些情况下,常量具有奇异的行为。像往常一样,拥有扎实的理论基础将使我们能够毫无意外地处理所有情况。


原文链接:http://codingdict.com