Java学习笔记#08 Java标识符

提到Java标识符的规则,很多博客都会告诉你标识符只能由字母、数组、美元符号($)、下划线(_)开头,且不能以数字开头。

网上广为流传的叙述似乎来自SCJP(Sun Certified Programmer for Java,SUN认证Java程序员)教程[1]:

  • 标识符必须以字母、货币符号($)或连接符(connecting character,如下划线_)开头。
  • 标识符不能以数字开头。
  • 在第一个字符之后,标识符可以包含字母、各种货币符号、连接符或数字的任意组合。
  • 对于一个标识符可以包含多少字符没有限制。
  • 不能将Java关键字作为标识符。
  • Java标识符区分大小写;foo和FOO是两个不同的标识符。
  • 非法的变量标识符用作方法或类的标识符同样是非法的。
  • Identifiers must start with a letter, a currency character ($), or a
    connecting character such as the underscore _.
  • Identifiers cannot start with a number.
  • After the first character, identifiers can contain any combination of letters, currency characters, connecting characters, or numbers.
  • There is no limit to the number of characters an identifier can contain.
  • You can’t use a Java keyword as an identifier.
  • Identifiers in Java are case-sensitive; foo and FOO are two different identifiers.
  • A legal identifier for a variable is also a legal identifier for a method or a class.

几条规则简单明了。
SCJP的表述同时指出不只是下划线可以作为标识符开头,而是包含下划线在内的连接符都可以作为标识符的开头。美元符号可以作为标识符的开头,而在一个字符之后标识符可以包含各种货币符号。
然而事实并没有那么简单。
下面这段代码是可以编译通过的:

int= 0; // 中文字符
int ⁀‿⁀ = 0; // 弯线和下弯线
int= 0; // 泰米尔卢比符号
int= 0; // 人民币/日元符号
if (== ⁀‿⁀ &&==) {
    System.out.println("Really?");
}

首先,弯线和下弯线都是Unicode中定义的连接符,符合SCJP的表述;
其次,泰米尔卢比符号和人民币/日元符号作为美元符号以外的货币符号,也可以作为标识符的开头,SCJP的表述似乎不够严谨;
但是“”是什么鬼???

Java标识符的规则到底是什么呢? Character类中我们可以找到isJavaIdentifierStart()方法判读一个字符能否作为Java标识符的开头,以及isJavaIdentifierPart()方法判断Java标识符能否包含该字符,通过这两个方法可以判断哪些字符能够组成合法的标识符。可以用这个方法判断一下“我”,的确是合法的标识符开头:

System.out.println(Character.isJavaIdentifierStart('我')); // true

这些合法字符中到底包含哪些字符呢,有没有一些规则来描述这些字符集合呢?
准确的叙述应当在Java语言规范[7]中查找:

  • 标识符是一个长度不受限制的由Java字母和Java数字的序列,序列首位必须是Java字母。
  • 标识符:标识符字符序列,但不能是关键字、布尔字面量(即true或false)或null字面量(即null)。
  • 标识符字符序列:Java字母 + 若干Java字母或数字
  • Java字母:任何属于Java字母的Unicode字符,即Character.isJavaIdentifierStart()返回true的字符
  • Java字母或数字:任何属于Java字母或数字的Unicode字符,即Character.isJavaIdentifierPart()返回true的字符
  • An identifier is an unlimited-length sequence of Java letters and Java digits, the first of which must be a Java letter.
  • Identifier: IdentifierChars but not a Keyword or BooleanLiteral or NullLiteral
  • IdentifierChars: JavaLetter [JavaLetterOrDigit]
  • JavaLetter: any Unicode character that is a “Java letter”
  • JavaLetterOrDigit: any Unicode character that is a “Java letter-or-digit”
  • A “Java letter” is a character for which the method Character.isJavaIdentifierStart(int) returns true.
  • A “Java letter-or-digit” is a character for which the method Character.isJavaIdentifierPart(int) returns true.

可见Java通过上述两个方法定义了Java字母、Java数字、Java字母或数字这几个集合。
标准中还提到“Java字母”不止包含我们通常所理解的ASCII码中的52个大小写英文字母,出于历史原因还包含了ASCII码中的下划线和美元符号。同时Java字母支持如今世界上仍在使用的大部分书写字母(most writing scripts in use in the world today),这其中包括了中文、日文和韩文的大型字符集,这就允许编程人员在他们的程序中用当地语言编写标识符。
而“Java数字”同理不只是10个ASCII阿拉伯数字,还包括Unicode中不同语言的数字。
这个叙述再次将合法标识符字符集的定义扩大,中文字符被囊括在“Java字母”的范畴内,自然是合法的标识符。

接下来看看到底有多少合法的Java标识符。
Character.MAX_CODE_POINT是Unicode中的最大码点(code point,即字符的编码),遍历所有码点,统计有多少个字符可以作为合法的Java标识符开头,有多少个字符可以作为合法的Java标识符,同时通过Character.isDigit()方法统计一下有多少“Java数字”:

int javaIdentifierPart = 0;
int javaIdentifierStart = 0;
int digit = 0;
int digitAndPart = 0;
for (int i = Character.MIN_CODE_POINT; i < Character.MAX_CODE_POINT; i++) {
    if (Character.isJavaIdentifierPart(i)) {
        javaIdentifierPart++;
    }
    if (Character.isJavaIdentifierStart(i)) {
        javaIdentifierStart++;
	}
	boolean isPart = Character.isJavaIdentifierPart(i) && !Character.isJavaIdentifierStart(i);
	// 统计数字
	if (Character.isDigit(i)) {
        digit++;
		// 统计不能是开头,但是能作为合法标识符的数字
    	if (isPart) {
        	digitAndPart++;
		}
	}
}
System.out.println("max code point " + Character.MAX_CODE_POINT);
System.out.println("part " + javaIdentifierPart);
System.out.println("start " + javaIdentifierStart);
System.out.println("part but not start " + (javaIdentifierPart - javaIdentifierStart));
System.out.println("digit " + digit);
System.out.println("digit And Part " + digitAndPart);

打印输出:

max code point 1114111
part 129053
start 125951
part but not start 3102
digit 630
digit And Part 630

可见1114111个Unicode字符中,有129053个合法字符,有125951个字符是合法开头字符,其中有3102个字符不能作为标识符开头,但是“Java数字”只有630个(且这630个数字都是合法标识符,都不能是标识符开头),剩下那两千多个字符又是什么呢?

查看Character.isJavaIdentifierStart()源码注释。发现有4类字符可以作为标识符开头:
isLetter()返回true的;
getType()返回LETTER_NUMBER的;
货币符号;
连接符(connecting punctuation character)。

查看Character. isJavaIdentifierPart()源码注释。发现有8类字符可以构成标识符:
letter;
货币符号;
连接符;
数字;
数字字母;
组合标记符;
非空格标记符;
isIdentifierIgnorable()返回true的码点。
Character.isIdentifierIgnorable()返回true的码点包括两类,一类是ISO控制字符中的非空格字符,一类是总体目录(General_Category)值为Character.FORMAT的字符

这里提到的总体目录是Unicode中的一个概念,Unicode中每个码点都有一个总体目录(General_Category)属性,该属性对所有码点做了基础的分类[3]。像上面提到的连接符确切而言是指Unicode中总体目录为Pc(Punctuation, connector)的字符。

从源码注释中可以得到对于Java标识符合法字符集的确切定义:
Unicode总体目录属性为:

Lu(Letter, uppercase);
Ll(Letter, lowercase);
Lt(Letter, titlecase);
Lm(Letter, modifier);
Lo(Letter, other);上面五项对应isLetter()返回true
Nl(Number, letter);对应getType()返回LETTER_NUMBER
Pc(Punctuation, connector);连接符
Sc(Symbol, currency)货币符号

的字符均是合法的Java标识符开头。值得一提的是Java9将下划线作为关键字,不允许单独的下划线作为标识符。

Unicode总体目录属性为:

Lu(Letter, uppercase); letter
Ll(Letter, lowercase);
Lt(Letter, titlecase);
Lm(Letter, modifier);
Lo(Letter, other);上面五项对应letter
Nl(Number, letter);数字字母
Pc(Punctuation, connector);连接符
Sc(Symbol, currency);货币符号
Nd(Number, decimal digit);(十进制)数字
Mc(Mark, spacing combining);组合标记符
Mn(Mark, nonspacing);非空格标记符
Cc(Other, control)中不是空格的字符;不是空格的控制符
Cf(Other, format)格式符

的字符均可以构成是合法的Java标识符。

验证一下:

public class Identifier {
    public static void main(String[] args) {
        int javaIdentifierPart = 0;
        int javaIdentifierStart = 0;
        int alphabetic = 0;
        int alphabeticAndStart = 0;
        int letter = 0;
        int letterAndStart = 0;
        int letterNumber = 0;
        int letterNumberAndStart = 0;
        int connector = 0;
        int connectorAndStart = 0;
        int currency = 0;
        int currencyAndStart = 0;
        int digit = 0;
        int digitAndPart = 0;
        int decimalDigit = 0;
        int combiningSpacing = 0;
        int combiningSpacingAndPart = 0;
        int nonSpacing = 0;
        int nonSpacingAndPart = 0;
        int control = 0;
        int controlAndPart = 0;
        int format = 0;
        int formatAndPart = 0;
        HashSet<Integer> digitTypeSet = new HashSet<>();
        HashSet<Integer> partNotStartTypeSet = new HashSet<>();
        for (int i = Character.MIN_CODE_POINT; i < Character.MAX_CODE_POINT; i++) {
            if (Character.isJavaIdentifierPart(i)) {
                javaIdentifierPart++;
            }
            if (Character.isJavaIdentifierStart(i)) {
                javaIdentifierStart++;
            }
            boolean isPart = Character.isJavaIdentifierPart(i) && !Character.isJavaIdentifierStart(i);
            if (isPart) {
                partNotStartTypeSet.add(Character.getType(i));
            }

            if (Character.isAlphabetic(i)) {
                alphabetic++;
            }
            if (Character.isAlphabetic(i) && Character.isJavaIdentifierStart(i)) {
                alphabeticAndStart++;
            }

            if (Character.isLetter(i)) {
                letter++;
            }
            if (Character.isLetter(i) && Character.isJavaIdentifierStart(i)) {
                letterAndStart++;
            }
            if (Character.getType(i) == Character.LETTER_NUMBER) {
                letterNumber++;
            }
            if (Character.getType(i) == Character.LETTER_NUMBER && Character.isJavaIdentifierStart(i)) {
                letterNumberAndStart++;
            }
            if (Character.getType(i) == Character.CONNECTOR_PUNCTUATION) {
                connector++;
            }
            if (Character.getType(i) == Character.CONNECTOR_PUNCTUATION && Character.isJavaIdentifierStart(i)) {
                connectorAndStart++;
            }
            if (Character.getType(i) == Character.CURRENCY_SYMBOL) {
                currency++;
            }
            if (Character.getType(i) == Character.CURRENCY_SYMBOL && Character.isJavaIdentifierStart(i)) {
                currencyAndStart++;
            }
            if (Character.isDigit(i)) {
                digit++;
                if (isPart) {
                    digitAndPart++;
                    digitTypeSet.add(Character.getType(i));
                }
            }
            if (Character.isDigit(i) && Character.getType(i) == Character.DECIMAL_DIGIT_NUMBER) {
                decimalDigit++;
            }
            if (Character.getType(i) == Character.COMBINING_SPACING_MARK) {
                combiningSpacing++;
                if (isPart) {
                    combiningSpacingAndPart++;
                }
            }
            if (Character.getType(i) == Character.NON_SPACING_MARK) {
                nonSpacing++;
                if (isPart) {
                    nonSpacingAndPart++;
                }
            }
            if (Character.getType(i) == Character.CONTROL) {
                control++;
                if (isPart) {
                    controlAndPart++;
                }
            }
            if (Character.getType(i) == Character.FORMAT) {
                format++;
                if (isPart) {
                    formatAndPart++;
                }
            }
        }
        System.out.println("max code point " + Character.MAX_CODE_POINT);
        System.out.println("part " + javaIdentifierPart);
        System.out.println("start " + javaIdentifierStart);
        System.out.println("part but not start " + (javaIdentifierPart - javaIdentifierStart));
        System.out.println();
        System.out.println("alphabetic " + alphabetic);
        System.out.println("alphabetic And Start " + alphabeticAndStart);
        System.out.println("letter " + letter);
        System.out.println("letter And Start " + letterAndStart);
        System.out.println("letterNumber " + letterNumber);
        System.out.println("letterNumber And Start " + letterNumberAndStart);
        System.out.println("connector " + connector);
        System.out.println("connector And Start " + connectorAndStart);
        System.out.println("currency " + currency);
        System.out.println("currency And Start " + currencyAndStart);
        System.out.println();
        System.out.println("digit " + digit);
        System.out.println("digit And Part " + digitAndPart);
        System.out.println("decimalDigit " + decimalDigit);
        System.out.println("digitType " + digitTypeSet);
        System.out.println("combiningSpacing " + combiningSpacing);
        System.out.println("combiningSpacing And Part " + combiningSpacingAndPart);
        System.out.println("nonSpacing " + nonSpacing);
        System.out.println("nonSpacing And Part " + nonSpacingAndPart);
        System.out.println("control " + control);
        System.out.println("control And Part " + controlAndPart);
        System.out.println("format " + format);
        System.out.println("format And Part " + formatAndPart);
        System.out.println("partNotStartType " + partNotStartTypeSet);
    }
}

输出:

max code point 1114111
part 129053
start 125951
part but not start 3102

alphabetic 127256
alphabetic And Start 125879
letter 125643
letter And Start 125643
letterNumber 236
letterNumber And Start 236
connector 10
connector And Start 10
currency 62
currency And Start 62

digit 630
digit And Part 630
decimalDigit 630
digitType [9]
combiningSpacing 429
combiningSpacing And Part 429
nonSpacing 1826
nonSpacing And Part 1826
control 65
control And Part 56
format 161
format And Part 161
partNotStartType [16, 6, 8, 9, 15]

可见:
标识符字符有129053个,标识符开头字符有125951个,不能作为标识符开头的标识符字符有3102个;
alphabet有127256个,其中可以作为标识符开头的有125879个;
letter有125643个,全部可以作为标识符开头;
字母数字有236个,全部可以作为标识符开头;

  • letter 125643个 + 字母数字236个 = 可以作为标识符开头的alphabet 125879个;

连接符10个,全部可以作为标识符开头;
货币符号62个,全部可以作为标识符开头;

  • letter 125643个 + 字母数字236个 + 连接符10个 + 货币符号62个 =标识符开头字符125951个

数字有630个,全部可以构成标识符,但不能开头;
组合标记符有429个,全部可以构成标识符,但不能开头;
非空格标记符有1826个,全部可以构成标识符,但不能开头;
控制符有65个,其中56个可以构成标识符,但不能开头;
格式符有161个,全部可以构成标识符,但不能开头;

  • 数字630个 + 组合标记符429个 + 非空格标记符1826个 + 控制符56个 + 格式符161个 = 不能作为标识符开头的标识符字符3102个

[1] http://www.java2s.com/Tutorial/SCJP/0020__Java-Source-And-Data-Type/JavaIdentifiers.htm
[2] https://dzone.com/articles/charsets-unicode-identifiers-in-java
[3] https://www.unicode.org/versions/Unicode13.0.0/ch04.pdf#G134153
[4] https://coderanch.com/t/266158/certification/connecting-character
[5] http://www.fileformat.info/info/unicode/category/Pc/list.htm
[6] https://www.fileformat.info/info/unicode/category/Sc/list.htm
[7] https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值