首页 > 技术文章 > 《JAVA核心技术 卷I》第三章

Solitary-Rhyme 2021-10-27 15:01 原文

第三章-Java的基本程序设计结构

1. 数据类型

1.1 char类型

  • char类型的字面量值需要用单引号括起来。如‘A’是编码值为65的字符常量,而"A"为包含一个字符A的字符串

  • char类型的值可以表示为16进制值(\u0000~\uFFFF)。例如,'\u2122'表示

  • 转义序列:

    • 类似\u的字符被称为"转义序列",除了\u外还有许多转义序列(在C语言中它们称为转义字符,如\n)
    转义序列 名称
    \b 退格
    \t 制表
    \n 换行
    \r 回车
    \ " 双引号
    \ ' 单引号
    \ \ 反斜杠
    • \u比较特殊,它可以出现在加引号的字符常量和字符串之外,这可能会导致一些隐性的错误
      • 如://\u000A is a newline 这行不会被当作注释,应为\u00A0会被替换为一个换行符
      • 同理:// look inside c:\users 会产生一个语法错误,因为\u后面没有接有效值
      • 转义序列会在解析代码之前处理,也就是说"\u0022+\u0022"实际上会被执行为""+""(空串连接)而不是"+"(包含+号的字符串)

1.2 Unicode和char类型

  • 码点:与一个编码表中的某个字符对应的代码值;码点采用16进制书写(U+0014 = 'A')

  • 代码平面:Unicode的码点可以分成17个代码平面;第一个代码平面被称为基本多语言平面(U+0000~U+FFFF);其余的16个代码平面瓜分U+10000到U+10FFFF(U+FFFFF + U+FFFF)

  • "UTF-16编码"采用不同长度的编码表示所有Unicode码点;在基本多语言平面中,每个字符用16bits表示,这些字符通常称为代码单元;而辅助字符(存在于其余16个代码平面中的字符)被编码为"一对连续的代码单元",采用这种编码对表示的各个值会落入基本多语言平面中未用的2048个值范围内(基本多语言面并没有用完,多出来的这一部分被划了一部分用于表示辅助字符的编码对,这片区域被称为替代区域

    • 举例:八元数集O,码点为U+1D546,不位于基本多语言平面,属于辅助字符;在UTF-16下,被编码为U+D835和U+DD46,这个编码对接下来会落入基本多语言平面中
  • 在Java中,char类型描述了UTF-16编码中的一个代码单元(也就是说,单个char类型变量,是无法表示辅助字符的)

  • 强烈建议不要在程序中使用char类型,除非确实需要处理UTF-16代码单元,最好将字符串作为抽象数据类型处理

1.3 boolean类型

  • Java中不能通过以下代码if(x = 0),x=0不会被转化为布尔值,也就无法通过判断

2.变量与常量

2.1 变量

  • 变量名必须是由字母开头的,以字母或数字构成的序列。Java中对于字母和数字的定义要比常见的大:字母包括"A-Z","a-z","_","$"或在某种语言中表示字母的任何Unicode字符(这意味着中文名变量理论上是可行的);数字包括"0~9"和在某种语言中表示数字的任何Unicode字符(但不包括辅助字符)

    • 不推荐在变量名中包含"$",它只用在Java编译器或者其它工具生成的名字中
  • 对于局部变量,如果可以从变量的初始值推断出它的类型,就不需要声明类型。只需要使用关键字var而无需指定类型

    var vacation = 12; //var自动识别为int
    var greeting = "Hello"; //var自动识别为String
    

2.2 常量

  • 关键字final表示这个变量只能被赋值一次;可以使用关键字static final设置一个类常量,类常量可以在一个类的多个方法中使用,类常量的定义位于main方法的外部,如果要让其他类也能访问这个常量,可以加一个public

2.3 枚举类型

  • 定义:有时候,变量的取值只在一个有限的集合内,针对这种情况,可以自定义枚举类型。枚举类型包括有限个命名的值。

    enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE};
    Size s = Size.MEDIUM;	//输出s显示MEDIUM
    

    Size类型的变量只能储存这个类型声明中给定的某个枚举值,或者特殊值null,null表示这个变量没有设置任何值

3.运算符

  • floorMod(Math类的一种方法),可以让负数取模时仍返回一个正的余数(符合一般数学定律);但如果取模的数是一个负数,则仍会返回一个负的余数

  • 如果希望得到一个更加准确的结果而不在意性能的话,可以使用StrictMath类;Math类还提供了一些方法使整数由更好的运算安全性

    • 例:常规运算下1000000000 * 3的计算结果将是一个负数(int类型溢出);但如果使用Math.multiplyExact(1000000000 * 3),就会生成一个异常。此外对于其它数据类型和运算类型还有对应的方法(addExact,substrateExact等)
  • 当用一个二元运算符连接两个值时,先要将两个操作数转换为同一种类型,然后再进行计算

    • 转化的优先级为double > float > long > int
  • Math库提供对浮点数进行四舍五入运算的方法Math.round(),该方法返回的数据类型为long类型

  • 如果试图将一个数值从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值

  • &&和||运算符是按照"短路"方式来求值的:如果第一个操作数已经能够确定表达式的值,第二个操作数就不必计算

    • 在一些特殊的题目中,这个特性会用来替代循环(比如作为递归的判断条件)
  • 位运算符

    • 处理整型类型时,可以直接对组成整数的各个位完成操作,这意味可以使用掩码技术得到整数中的各个位。位运算符包括:&(and),|(or),^(xor),-(not)
    • ">>"和"<<"运算符可以将位模式左移或右移,需要建立位模式来完成位掩码的时候,这两个运算符会很方便;">>>"运算符会使用0填充高位
    • 移位运算符的右操作数(位于运算符右边的数字)需要完成模32(25)的运算;若左操作数为long类型,则需要对右操作数模64(26)
    运算符 结合性
    [].() (方法调用) 从左向右
    !,~,++,--,+(一元运算),-(一元运算),(),new,()(强制类型转换) 从右向左
    *,/,% 从左向右
    +,- 从左向右
    <<,>>,>>> 从左向右
    <,<=,>,>=,instanceof 从左向右
    ==,!= 从左向右
    & 从左向右
    ^ 从左向右
    | 从左向右
    && 从左向右
    || 从左向右
    ?: 从右向左
    =,+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>=,>>>= 从右向左

    一元运算:从一个已知数中得出新的数,+-号的一元运算指的是其正负号的职能

    结合性:决定同级的运算符的运算方向,如从左向右结合性的运算符,运算的时候顺序是从左向右运算

4.字符串

4.1 字符串特性

  • String类中没有提供修改字符串中某个字符的方法;由于这一特性,String类对象被称为是不可变的;不可变字符串有一个优点:编辑器可以让字符串共享,这一点优化了性能

    • 如果复制一个字符串变量,原始字符串与复制字符串共享相同的字符
  • 检查字符串之间是否相等可以使用equals方法,而如果要忽略大小写则可以使用equalsIgnoreCase

    "Hello".equalsIgnoreCase("hello"); //不要使用"=="比较字符串!
    

4.2 字符串相关方法

  • substring()

String类的substring方法可以从一个较大的字符串中提取出一个子串

String greeting = "Hello";
String s = greeting.substring(0,3); //s为"Hel"
//substring方法的第二个参数是不想复制的第一个位置,以上个例子为例,在substring中从0开始计数,直到3为止,但不包含3
  • join()

如果需要把多个字符串放在一起,并用一个界定符分隔,可以使用静态join方法

String all = String.join("/","S","M","L","XL");	//all为"S/M/L/XL"
  • repeat()

    String repeated = "Java".repeat(3);	//repeated为"JavaJavaJava"
    
  • StringBuilder()

    有时候需要使用较短的字符串构建字符串,传统的字符串拼接方式效率比较低,此时可以使用StringBuilder,在构建完后使用toString方法返回一个String对象即可

    StringBuilder builder = new StringBuilder();
    builder.append(ch);	//拼接一个字符
    builder.append(str);	//拼接一个字符串
    String compString = builder.toString();
    

4.3 代码单元

  • 字符串由char值序列组成,这意味着码点和代码单元的知识点也可以应用于字符串中;length方法将返回采用UTF-16编码表示给定字符串所需要的代码单元数量;想要得到码点数量(实际的长度)。可以使用codePointCount()方法

    String greeting = "Hello";
    int n = greeting.length(); //n == 5
    int cpCount = greeting.codePointCount(0,greeting.length());
    

    调用s.charAt(n)将返回位置n的代码单元,想要得到第i个码点,可以使用codePointAt(n)

    char first = greeting.charAt(0);
    int index = greeting.offsetByCodePoints(0,1);
    int cp = greeting.charAt(index);
    

    如此关注码点和代码单元,是因为如果字符串中存在辅助字符,可能会造成charAt()等方法的出错,因为这类字符占用了两个代码单元。为了避免这一问题,尽量不要使用char类型

5.输入与输出

5.1 Scanner相关

  • 若要读取用户的输入,首先需要新建一个对象,然后再指定接收的类型;若要读取一整行输入,可以使用nextLine();若要读取一个单词,可以使用next();读取整数使用nextInt(),读取浮点数使用nextDouble

    Scanner in = new Scanner(System.in);
    String name = in.nextLine();
    String firstName = in.next();
    int age = in.nextInt();
    
  • 因为输入是可见的,Scanner类不适合从控制台读取密码(因为可能会暴露用户信息),可以使用Console类来实现

    Console cons = System.console();
    String username = cons.readLine("user name:");
    char[] password = cons.readPassword("Password:");
    

    为了安全起见,返回的密码应该存放在一个字符数组中,而不是字符串中。在对密码处理完成后,应该马上用一个填充覆盖数组元素。Console对象只能每次读取一行输入,而没有能读取单个单词或者数值的方法

    • Console API相关:

      static char[] readPassword(String prompt, Object ... args)

      static String readLine(String prompt, Object ... args)

      显示字符串prompt(提示符)并读取用户输入,直到输入行结束。args可以用来提供格式参数

    • Scanner API相关:

      boolean hasNext():检测输入中是否还有其它单词

      boolean hasNextInt(),boolean hasNextDouble():检测是否还有下一个表示整数或浮点数的字符序列

5.2 格式化字符串(printf)相关

  • Java沿用了C语言的printf方法来格式化数值

    System.out.printf("8.2%f",x);
    
转换符 类型
d 十进制整数
x 十六进制整数
o 八进制整数
f 定点浮点数
e 指数浮点数
g 通用浮点数
a 十六进制浮点数
s 字符串
c 字符
b 布尔
h 散列码
tx/Tx 日期时间
% 百分号
n 与平台有关的行分隔符
  • 此外,可以指定控制格式化输出外观的各种标志

    System.out.printf(""%.2f",10000.0/3.0);	//打印3,333.33
    
标志 目的
+ 打印正数和负数的符号
空格 在正数之前添加空格
0 数字前面补0
- 左对齐
( 将负数括在括号内
, 添加分组分隔符
#(对于f格式) 包含小数点
#(对于x或0格式) 添加前缀0x或0
$ 指定要格式化的参数索引(具体解释见P58)
< 格式化前面说明的数值(具体解释见P58)
  • 注意事项:

    1. 参数索引值从1开始,而非从0开始

    2. 可以使用s转换符格式化任意的对象。对于实现了Formattable接口的对象,将调用这个对象的formatTo方法;对于未实现这个接口的对象,将调用toString方法将这个对象转换为字符串

    3. 可以使用静态的String.format方法创建一个格式化的字符串,而不打印输出

      String message = String.format("Hello, %s",name);
      
    4. 对于日期与时间的格式化,应当使用java.time包中的方法,而不是过时的格式化语句

    5. 格式说明符语法图:

5.3 文件的输入与输出

  • 读取一个文件也需要构造一个Scanner对象

    Scanner in = new Scanner(Path.of("mylife.txt"),StandardCharsets.UTF_8);
    
    • 如果路径中包含反斜杠符号,就需要在每个反斜杠符号前面额外加一个反斜杠

      • 例:Path.of("C:\\myfile.txt");
    • 上述例子中制定了UTF_8编码,如果省略字符编码,则会使用运行这个Java程序的机器的"默认编码",这可能带来兼容性之类的问题。因此,读取文件前最好先知道文件的编码类型

  • 写入文件需要构造一个PrintWriter对象,构造器中需要提供文件名和字符编码

    PrintWriter out = new PrintWriter("myfile.txt",StandardCharsets.UTF_8);
    //若文件不存在,则会自动创建该文件
    
  • 当指定一个相对文件名(相对路径)时,文件位于Java虚拟机启动目录的位置。如果在命令行下执行以下命令启动程序:java MyProg,启动目录就是命令解释器的当前目录(cmd的当前运行目录)

    如果使用IDE,那么启动目录由IDE决定,可以使用下列代码找到目录位置:String dir = System.getProperty("user.dir");

    嫌麻烦也可以使用绝对路径,但兼容性较差

6.流程控制

  • switch-case中case的标签可以是:类型为char,byte,short,int的常量表达式;枚举常量;字符串字面量

  • Java提供了一种带标签的break/continue语句,类似于goto可以跳出多层嵌套的语句,非常方便

        public void labelTest(){
            Scanner in = new Scanner(System.in);
            int n;
    
            read_data:	//标签
            while(true)
            {
                for(;true;)
                {
                    System.out.print("Enter a number >= 0:");
                    n = in.nextInt();
                    if(n < 0){	//如果n<0则break出外层循环
                        break read_data;
                    }
                }
            }
    
            System.out.println("Break Success!!");
        }
    //事实上标签可以应用于几乎任何的语句块,可以方便的跳出(但不能跳入)。但并不提倡过多的使用这个特性
    
  • 注意事项

    • 在C中,允许在嵌套的块中定义一个重名变量,在内层定义的变量会覆盖在外层定义的变量。而在Java中不允许这么做

    • for语句的3个部分应该对同一个计数器变量进行初始化,检测和更新。若不遵守这一规则,编写的循环常常晦涩难懂

    • 在循环中,检测两个浮点数是否相等需要额外小心

      • 例如这个for循环:for(double x = 0;x != 10;x+=0.1),由于舍入的误差,可能永远达不到精确的最终值从而一直循环下去。在这个循环中,由于0.1无法精确的用二进制表示,所以x将从9.999...跳到10.099...

7.大数

  • 浮点数值不适用于无法接受舍入误差的金融计算。如果在数值计算中不允许有任何舍入误差,就应该使用BigDecimal类

  • 如果基本的整数和浮点数精度不能够满足要求,那么可以使用Math包中的两个类:BigInteger和BigDecimal,这两个类可以处理包含任意长度数字序列的数值。BigInteger类实现任意精度的整数运算;BigDecimal可以实现任意精度的浮点数计算

  • 使用静态的valueOf方法可以将普通的数值转换为大数:BigInteger a = BigInteger.valueOf(100); 对于更大的数,可以使用一个带字符串参数的构造器:BigInteger("25554648464846468468468");

  • 不能使用算术运算符(+或*)处理大数,而需要使用大数类提供的add和multiply方法

8.数组

  • 定义数组

    • 在Java中,提供了一种创建数组对象并同时提供初始值的简写形式。这个语法中不需要使用new,甚至不用指定长度
      • 例:int[] smallPrimes = {2,3,5,6,8};
    • 使用{}初始化数组时,最后一个值后面允许有逗号,这样方便以后手动添加新代码
    • 在Java中允许有长度为0数组;但长度为0的数组与null并不同
    • for(variable : collection) statement;foreach循环定义一个变量用于暂存集合中的每一个元素,并执行相应的语句(statement)。collection这一集合表达式必须是一个数组或者是实现了Iterable接口的类对象
  • 打印数组

    • 使用Arrays.toString(a),可以快速简单的打印数组中的元素

    • foreach循环语句不能自动处理二维数组的每一个元素。要想访问二位数组a的所有元素,需要使用两个嵌套的循环

      • 例:for(double[] row : a)

        ​ for(double value : row)

      • 想要快速的打印一个二位数组的数据元素列表,可以调用:Arrays.deeptoString(a)

  • 拷贝数组

    • 直接使用“=”进行拷贝,两个变量将引用同一个数组

      int[] luckyNumbers = smallPrimes;
      luckyNumbers[5] = 12; //现在smallPrimes[5]也等于12
      
    • 如果希望将一个数组的所有值拷贝到一个新的数组中去,就要使用Arrays类的copyOf方法

      int copied = Arrays.copyOf(luckyNumbers,luckyNumbers.length);
      //第二个参数是新数组的长度,这个方法也可以用来调整数组的大小
      luckyNumbers = Arrays.copyOf(luckNumbers,luckNumbers.length * 2);
      
  • 与C类似,Java的应用程序也可以在运行开始时添加命令行参数

    • 例:java Message -g cruel world

      ​ args数组将会包含以下内容:args[0]:"-g"; args[1]:"cruel"; args[2]:"world";

推荐阅读