chapter 3 基本程序设计结构


作者:Denis

版本:2.1

完成时间:2022/10/14

编写地点:中国山西省

3.1 编写Java程序的规则


一个简单的Java程序是这样的

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

有以下规则需要遵守:

  1. 对于变量、类名、方法,Java区分大小写。
  2. Java应用程序的全部内容需要放在类中,也就是说,无论问题规模的大小,都需要把代码装在类(class)中。
  3. 类名要以字母开头,且不能使用Java的关键字和保留字
  4. 每一个Java文件只允许有一个公共类(即public class),但允许拥有多个其他类(即class
  5. 源代码的文件名需要与公共类一致
  6. 每一句代码结束都需要加上分号;

编写代码的规范:

  1. 对于类名和方法名,应该使用驼峰命名法,形如camelCase。类名首字母应该大写,方法和变量名首字母应该小写。
  2. 常量名应该全部大写,单词与单词之间使用下划线分割,形如MAX_VALUE
  3. 每一行代码不宜过长,过长的代码可以换行显示。
  4. 不要编写返回可变对象引用的访问器方法,否则,外部人员可能会获得这个对象,造成Student对象被篡改。
//错误演示
public class Student{
    //Date拥有更改器方法SetTime()
	private Date birthday;
	public getBirthday(){
		return this.birthday;
	}
}
  1. 如果要返回一个可变对象的引用,应该使用克隆clone方法
  2. 通过类名调用静态方法,而不是方法名
  3. 初始化块应该放在声明的字段之后
  4. 字段应该尽量设置为private,如果设置为包访问类型可能其他类会访问到这个字段。其他类既包括自己编写的,也有其他人员恶意加入到包内中的。

当运行程序时,总是从指定程序的main方法开始执行。

如果main方法正常执行完毕,则Java程序的退出码为0,这一点类似于C++的return 0;

如果期望程序终止时返回其他退出码,需要调用System.exit(int parameter)

3.2 注释


Java语言有三种类型的注释,分别为单行注释、多行注释、javadoc注释。注释不会影响代码的执行。

需要注意:注释不要嵌套。

// 单行注释
/*
   多行注释
*/
/**
  *Javadoc注释
*/

Javadoc注释可以自动的生成文档

使用javadoc工具可以构建一个像Java手册一样的HTML文档

在编写javadoc注释时,可以添加标记,这些标记以@符号开头,如@return

注释中的文本被称为自由文本,自由文本的第一句话被当作概要句,放在概要页中。

自由文本可以使用HTML标签修饰符,像<strong></strong>等等

如果要在自由文本中输入代码,需要使用{@code ...},这样就不用担心尖括号的字符转义了。

如果想让生成的网页手册显示图片,应该把图片资源放在源文件目录下的子文件夹doc-files,并在文档中显式使用图片

<img src="doc-files/uml.png" alt="UML diagram">

不同位置的注释

  • 包注释

放在最前面即可,如果想产生包注释,需要提供另外的文件,放在每一个包目录里

一种是提供一个package.html文件,将自动抽取body标签中的内容

一种是提供一个package-info.java文件,只包含package语句和开头的javadoc注释,不能包含更多的代码

  • 类注释

应该放在import语句之后,类定义之前

  • 方法注释

应该放在定义方法之前,可以使用如下标记

标记

解释

@param

一个方法的所有@param需要放在一起,作为对方法参数的解释

@return

对返回值做解释

@throws

对可能抛出的异常做解释

  • 字段注释

应该放在每个字段之前,通常对静态常量建立文档

通用注释

标记

解释

@author

可以有多个author

@version

版本号

@see

链接到其他javadoc部分或者外部文档,会在网页的see also栏目创建超链接,可以使用多个

@link

同see

@since

来源于某个版本

@see具体的用法

/**
    @see <a href="www.xxx.com/xxx.html">xxx</a>
    //文本出现在see also栏目中
    @see "xxx"
    //省略包名或方法则定位到本类
    @see com.runbit.Student#exam(int id)
    //指向任何其他类或方法,在哪里插入都行
    {@link package.class#method}
*/

提取注释

生成注释的HTML文件,并放在指定的目录里

命令:javadoc -d directoryName nameOfPackage1 nameOfPackage2 ...

如果是无名包

命令:javadoc -d directoryName *.java

可以在命令中加入-author -version选项,以此让文档包含作者和版本号,默认这些信息会被抹去

其他javadoc命令选项

选项

解释

-link http://docs.oracle.com/javase/9/docs/api *.java

为所有使用到的标准类添加超链接

-linksource

每个源文件转为HTML,不会着色,但有行号

-overview filename

为所有源文件提供一个概要注释,这些注释要放在一个HTML文件中(即对应的filename),自动提取body标签内的内容,点击生成好的文档中的Overview即可查看

更多内容查阅:http://docs.oracle.com/javase/9/javadoc/javadoc.htm

3.3 数据类型


Java是一门强类型语言,需要为每一个变量申明一种类型。

Java的数据类型包括基本数据类型和对象引用。

对于编译器可推导的类型,可以使用var代替类型【since Java 10】但只能用在方法中的临时变量。

基本数据类型一共有8种。

基本数据类型

存储需求

byte

1字节

short

2字节

int

4字节

long

8字节

float

4字节

double

8字节

char

Java采用UTF-16编码,具体大小视代码单元(code unit)而定,为2~4字节。

boolean

单独使用4字节,使用boolean数组每一个元素1字节。具体情况不确定,依赖于具体的虚拟机实现。

整数类型是表中前四种,通常int类型最为常用,但最多表示到21亿多,超出这个数值需要使用long

byte和short用于特定的环境,例如底层文件处理。

长整数(long)数值后面有一个L或者l,如20000L

注意Java没有任何无符号(unsigned)形式的int、long、short、byte类型。

【实际上也可以将有符号数解释为无符号数,例如用byte表示0~255,但很少使用,具体去百度】

一般而言,很少使用float,除非存储空间很宝贵或者其他情况

可以使用十六进制表示浮点数

0.125 = 2-3 = 0x1.0p-3

p表示指数,尾数采用十六进制,指数采用十进制,指数的基数为2

表示溢出和出错情况的三个特殊的浮点值,实践中很少使用

  • 正无穷大 Double.POSITIVE_INFINITY、Float.POSITIVE_INFINITY
  • 负无穷大 Double.NEGATIVE_INFINITY、Float.NEGATIVE_INFINITY
  • 不是一个数 Double.NaN、Float.NaN
  • 可以使用Double.isNaN(x),判断x是不是一个数

区分不同的数据类型,可以看具体数值的后缀,后缀不区分大小写

基本数据类型

后缀

long

20000000L

float

20.12f(必须指定,否则视为double)

double

20.12d(不是必须指定)

在数字前加不同的前缀可以表示不同的进制

进制

举例

二进制

0b1011

八进制

07255

十进制

1234(无前缀)

十六进制

0xa5bf

为增加易读性,可以对数字字面量增加下划线,Java编译器会自动擦除这些下划线。

例如一百万可以写为1_000_000

char最初表示单个字符,随着emoji符号的出现,有一些字符需要两个代码单元

emoji符号属于辅助字符,其编码从U+10000到U+10FFF,辅助字符的代码单元一个落在(U+D800 - U+DBFF),另一个落在(U+DC00 - U+DFFF)

char类型表示UTF-16编码中的一个代码单元(code unit)应该尽量避免使用char。

使用char类型变量,需要使用一对单引号''引起来一个字符

以下语句对char类型变量的赋值是合法的。

char c1 = 'A';
char c2 = 65; //可以为其赋值数字,返回其ASCⅡ码对应的字符
char c3 = '\u2122'; //可以为其赋值为十六进制的值,范围(\u0000~\uFFFF)
int i = 66;
char c4 = i; //错误,不能给一个int变量。
char c4 = (char)i; //可以,使用类型强转

转义序列\u可以出现在字符串中,或者外边。

String str = "\u2122";
//代替[]
public static void main(String\u005b\u005d args)

除了转义序列\u外,还可以使用其他转义序列,同样符合规则。

转义序列

Unicode值

实际含义

\b

\u0008

退格(backspace)

\t

\u0009

制表

\n

\u000a

换行

\r

\u000d

回车(回车不等于换行,回车把光标定位到该行最前面)

\"

\u0022

"

\'

\u0027

'

\\

\u005c

\

boolean值只有true和false两种状态,不参与类型转换。

3.4 枚举类型


当变量的值在一个有限的集合内的时候,可以申明为枚举,需要注意,枚举是类。

枚举集合内的值都是常量

所有的枚举类型都是Enum类的子类,继承了Enum类的许多方法。

public enum Size{
	SMALL,MEDIUM,LARGE,EXTRA_LARGE;
}

使用枚举

枚举变量只能接受声明中给定的枚举值,或者是null

Size s = Size.MEDIUM;

比较枚举值

直接使用==,不需要使用equals

可以为枚举定制构造器、方法、字段

构造器必须是私有(private)的,即使不显式声明为private,编译器也会认为是private,而不是包内访问类型

public enum Size {

    SMALL("S"),
    MEDIUM("M"),
    LARGE("L"),
    EXTRA_LARGE("XL");

    private String abbreviation;

    private Size(String abbreviation){
        this.abbreviation = abbreviation;
    }

    public String getAbbreviation(){
        return abbreviation;
    }
}

每个枚举类型都有一个静态的values方法,返回一个包含全部枚举值的数组

Size[] values = Size.values();

ordinal方法返回这个枚举常量在枚举集合中的位置,位置从0开始

System.out.println(Size.LARGE.ordinal());

输出2

枚举的其他常用方法

  1. 返回枚举常量的字面量字符串
Size.SMALL.toString(); // 字符串"SMALL"
  1. 设置一个枚举常量
Size s = Enum.valueOf(Size.class, "SMALL"); // s现在为Size.SMALL
  1. 与其他枚举常量比较
Size s1 = Size.MEDIUM;
Size s2 = Size.SMALL;
s1.compareTo(s2); //返回1,因为MEDIUM在SMALL后面一位。如果在后面N位返回N;在前面N位,返回-N;如果两个枚举常量相等返回0

3.5 数学函数


Math类提供了很多有用的方法,介绍几个没见过的

  • Math.atan2(y, x);

输入x-y坐标系的一条射线上任意的坐标,返回该射线与x轴的角度

Math.atan2(1, 1); // 返回π/4,也就是45°
  • Math.exp(x)

返回ex

  • Math.E

自然对数e

  • Math.log(x)

以e为底x的对数

  • Math.log10(x)

以10为底x的对数

  • Math.round(x)

四舍五入,返回long类型数据

3.6 数据类型转换


3.6.1 隐式转换


当一个二元运算符连接两个值时,低类型的数据会被自动提升为高类型

double x = 1 + 2.5; // 1先转换为1.0然后参与运算
double x2 = 2.5f + 2.6d; // 2.5f先转换为2.5d然后参与运算
float y = 1 + 2.6f; // 1先转换为1.0f然后参与运算
float z = 1000L + 3.6f;//1000L先转为1000.0f然后参与运算
float z = 1000L + 3.6;//错误,试图将double转为float
long m = 1000L + 25; // 25先转换为25L然后参与运算
char a1 = 65;
short a2 = 72;
byte a3 = 14;
int res = a1 + a2 + a3; //参与运算时,char、short、byte会转换为int

3.6.2 强制类型转换


当高类型转换为低类型时,需要强制转换。强制转换会丢失一部分信息。

double x = 9.6;
int nx = (int)x; // nx = 9

3.7 逻辑运算符


短路操作

人们经常使用&&、||这类的运算符,但其实,&、|也可以实现这样的功能。这两类运算符有一些区别

&&用于判断且逻辑

A && B,当且仅当A和B都为真才返回真,否则返回假。

为了提升效率,如果A为假,就不会考虑B的真假。

A & B与上面提到的功能一样

不同的是,即使A为假,依旧会验证B的真假性

对于|| 与 | 是一个道理

位运算符

包括&("and") |("or") ^("xor") ~("not")

0b1001 & 0b1100 = 0b1000; // 8
0b1001 | 0b1100 = 0b1101; // 13
0b1001 ^ 0b1100 = 0b0101; // 5
~0b1001 // -10

可能会对最后一个结果感到困惑,科普一下二进制的知识

计算机对数据以二进制进行存储。二进制有三种方式表示一个数:原码、反码、补码。

其中,原码是给人看的。补码是给计算机计算的。补码的出现使计算机的加减法得到统一。

0b1001 = 9

1001是其原码,在计算机中,正数的原码、反码、补码都一致,其最高位为0,而负数的原码、反码、补码不尽相同,其最高位为1。

因为int类型是4字节,所以完整的原码为

00000000 00000000 00000000 00001001

对其取反为

11111111 11111111 11111111 11110110

这个结果是补码,转换为原码,人类就可以看懂了

补码 - 1 = 反码

11111111 11111111 11111111 11110101

取反(最高位保持原状)得到原码

10000000 00000000 00000000 00001010

这个值为-10

位移运算符(针对二进制)

>>表示右移,使用符号位补充高位

<< 表示左移

>>>表示右移,使用0补充高位

总结:使用逻辑运算符,可以加快程序的运行速度。具体的使用场景可以知乎搜索。
注意:移位的右操作数要完成模32位运算,比如1<<35, 实际效果为1<<3 = 8 因为 35%32 = 3,如果左操作数是long类型,则对右操作数模64

3.8 字符串


Java中,字符串是不可变的(同样不可变的有基本类型的包装类)

字符串"Hello"永远都是Hello,不可以对其进行修改,但可以让字符串变量指向新的字符串。

不可变的字符串有一个优点:可以实现字符串的共享。

具体来说,可以想象为把字符串对象放在一个公共的存储池里,变量直接引用即可。

如果程序需要不断的从输入中拼接字符串,可以使用StringBuilder类,来避免生成许多中间的无用字符串。

//使用StringBuilder构建字符串。
StringBuilder builder = new StringBuilder();
builder.append(str);
String completedString = builder.toString();

如果程序需要以多线程的方式添加和删除字符,可以使用StringBuffer,它与StringBuilder的API一样。

获取字串

subString(start, end);

获取原字符串[start, end)处的内容,索引从0开始。

拼接

可以直接使用+进行拼接

如果要拼接多个字符串每个字符串之间使用一个特殊的分隔符分隔可以使用join方法

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

重复多个字符串拼接成一个新串可以使用repeat方法

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

检测字符串是否相等

使用equals方法,不要使用==方法。因为完全有可能内容相同的字符串放在不同的位置上(比如使用+、substring操作得到的字符串)。

码点与代码单元

字符串是由char值序列组成,最常见的Unicode字符需要一个代码单元表示,而辅助字符则需要2个

一个码点表示一个Unicode字符

String str = "Hello";
str.length(); // 返回代码单元的数量,这里返回5
str.codePointCount(); //返回码点的数量,这里返回5
str.charAt(n); //返回第n个代码单元的字符
//获取第i个码点
String str = String.valueOf(Character.toChars(0x1D546))+"Hello";// 𝕆Hello
System.out.println(str.length());//7
int index = str.offsetByCodePoints(0, i);
int cp = str.codePointAt(index);

将字符串转换为字符数组

int[] codePoints = str.codePoints().toArray();

将字符数组转换为字符串

String str = new String(codePoints, 0, codePoints.length);

其他常用方法

方法

返回类型

描述

charAt(int index)

char

返回代码单元,不建议使用

codePointAt(int index)

int

返回码点

offsetByCodePoints(int index, int codePointOffset)

int

从index开始,codePointOffset后的码点索引

compareTo(String other)

int

依据字典序返回数字

codePoints()

IntStream

返回整形流,调用toArray返回整形码点数组

isBlank()、isEmpty()

boolean

如果为空,或者全为空白,返回true

equalsIgnoreCase(String other)

boolean

与另一个字符串比较,忽略大小写

startsWith(String prefix, int offset)

boolean

从offset个字符开始,是否以prefix开头

endsWith(String prefix)

boolean

是否以prefix结尾

indexOf()

int

四种重载方式,返回从指定位置开始(默认从头部开始)检索第一次出现字串或码点的位置,如果不存在,返回-1

spilt(String regex)

String[]

两种重载方式,依据分割符获取分割后的字符串数组

lastIndexOf()

int

四种重载方式,返回从指定位置开始(默认从尾部开始)检索最后一次出现字串或码点的位置

codePointCount(int beginIndex, int endIndex)

int

从beginIndex,和endIndex - 1之间码点的个数

replace(CharSequence target, CharSequence replacement)

String

返回被所有被替换后的新字符串,String和StringBuilder都可以作为参数

toLowerCase()、toUpperCase()

String

大写改小写、小写改大写

trim()、strip()

String

除去开头和尾部小于等于U+0020的字符和空格

StringBuilder的其他常用方法

方法

返回类型

描述

appendCodePoint(int cp)

StringBuilder

追加一个码点,将一个或两个代码单元添加到末尾,并返回this

setCharAt(int i, char c)

void

将第i个代码单元设置为c

insert(int offset, char c)

StringBuilder

在offset处插入字符c并返回this,insert有多种重载方法

delete(int start, int end)

StringBuilder

删除从start到end - 1的代码单元并返回this

3.9 输入与输出


明文输入:Scanner

使用Scanner获取输入,需要使用System.in对其进行初始化

获取输入的内容可以使用nextline、next【获取一个单词,使用空格分割】、nextInt等等

使用hasNext判断输入中是否还有其他单词,使用hasInt判断输入中是否还有其他数字

密码输入:Console

import java.io.*;
public class TestConsole{
	public static void main(String[] args) {
        //从控制台启动可以使用,否则console是否可用取决于系统
		Console console = System.console();
		String username = console.readLine("username:\n");
        //密码放在一个字符数组里,处理后应该马上使用别的值覆盖数组
		char[] password = console.readPassword("password:\n");
		System.out.println(username);
		System.out.println(new String(password, 0, password.length));
	}
}

仿C语言的格式化输出

如果使用print方法输出1000.0 / 3.0,会得到很多的尾数数字

如果只想看到小数点后两位,可以使用如下办法

System.out.printf("%8.2f", 1000.0 / 3.0);

可以为printf提供多个参数

System.out.printf("Hello, %s. Next year, you will be %d", "denis", 21);
// Hello, denis. Next year, you will be 21

常见的转换参数(省略%)

参数

描述

s

字符串

x

十六进制数

o

八进制数

f

定点浮点数

e

指数浮点数

a

十六进制浮点数

c

字符

b

布尔值

h

散列码

%

百分号

n

与平台有关的行分隔符

实际上%s可以格式化任意对象,如果这个对象实现了Formattable接口,就调用其formatTo方法,否则调用其toString方法。

除此之外,还可以使用标志修饰符,使输出的数字更具有有个性

例如使用+修饰符使输出的数字带正负号,可以同时使用多个标志修饰符

标志

目的

示例

+

使输出的数字带正负号

+333.33

(

将负数括在括号内,正数则不做修饰

(-333.33)

,

添加分组修饰符

3,333,333,333.33

#(对于x和o格式)

添加0x或者0

0x99fb,077

$ | 对指定位置的参数起作用,例如"%1\(d %1\)x", 16将以10进制和十六进制打印第一个参数

16 10

<

格式化前面的数值,例如"%1\(d %1\)x,%2$d %<x", 16, 20将以10进制和十六进制打印同一个参数

16 10 20 14

0

数字前面补0 例如 "%08.2f", 300.254

00300.25

空格

正数前添加空格

300

-

左对齐

300

#(对于f格式)

包含小数点 例如"%#2.0f", 300

300.

l

输出长整型

如果不想立刻输出上述字符串,可以使用String类的format方法创建一个String对象

String message = String.format("%8.2f",32.15);

输出时间的格式化串为%t+转换符,例如%tc可以输出完整的时间,其对象为Date类的对象

对于旧的代码,可以看到这种痕迹,编写新的代码不应该使用这种方式

Date date = new Date();
//当前日期:2022-10-14
System.out.printf("%tF\n",date);
//当前时间:10:00:00
System.out.printf("%tT\n",date);
//当前时间(12小时制)
System.out.printf("%tr\n",date);
//当前时间(时分)
System.out.printf("%tR\n",date);
//当前完整星期
System.out.printf("%tA\n",date);
//当前缩写星期
System.out.printf("%ta\n",date);
//目前是一年中的第几天
System.out.printf("%tj\n",date);
//时区:伊尔库兹克时间
System.out.printf("%tZ\n",date);

//输出结果
2022-10-14
10:46:35
10:46:35 上午
10:46
星期五
周五
287
IRKT

读取和写入文件

可以先使用Scanner对象进行处理,后续还有别的方法

String path = "D:\\Desktop\\sql.txt";
Scanner in = new Scanner(Path.of(path), StandardCharsets.UTF_8);
while(in.hasNext()){
    //读取文件内容并进行输出
    System.out.println(in.nextLine());
}
//创建输出对象,可以像调用System.out.println、print、printf调用其方法
PrintWriter out = new PrintWriter(path,StandardCharsets.UTF_8);
out.print("这是新添加的文字");
out.close();
//输出用户当前路径
System.out.println(System.getProperty("user.dir"));

3.10 控制流程


java有三种控制流程,即条件、循环、选择

条件:表达式为真会执行

循环:for、while、do...while,其中最后一种循环至少执行一次

选择:匹配对应的case,然后执行代码,case可以为:char、byte、short、int、枚举常量、字符串

增强for循环 for each

for(variable : collection)
	statement

使用这种for循环collection必须是数组,或者是实现了Iterable接口的类对象。

如果想快速打印数组的所有值,可以使用Arrays类的静态方法

Arrays.toString(collection);

使用goto

Java将goto作为保留字,但实际中没有用到。可以使用标签来作为goto的替代品,以此来跳出深层循环。

可以在任何地方使用标签,在break和continue后可以添加标签。

A:
for(int i = 0; i < 10; i++){
	...
	B:
	for(int j = 0; j < 10; j++){
		...
		if(...)
			break A;
		else
			continue A;
	}
}

3.11 大数


大数有两种类型:BigDecimal和BigInteger

可以处理任何长度的数值(假如long和double都无法表示的数值)

构建大数对象有三种方法。

  1. 将普通的数转换为大数
BigDecimal a = BigDecimal.valueOf(100.21);
//这种方法返回 100 / 10^2 = 1.00 
BigDecimal s = BigDecimal.valueOf(100, 2);
BigInteger b = BigInteger.valueOf(100L);
  1. 构建一个很大的数
BigInteger reallyBig = new BigInteger("11111111111111111111111111111111111111111111111111");
  1. 使用大数常量
BigDecimal c = BigDecimal.ONE; //1 0 10

不能使用常见的运算符参与大数运算,而应该使用大数自带的方法

如add、multiply,divide,subtract,mod,sqrt

compareTo也是大数的一个方法,如果两个大数相等返回0,否则返回1或者-1

注意:BigDecimal中的divide方法如果结果是一个无限循环小数会报错,如果要得到一个舍入的结果,应该使用divide(BigDecimal divisor, RoundingMode roundingMode),这样做会保留到分子的精度。四舍五入时可以使用RoundingMode.HALF_UP

3.12 数组


拷贝数组

直接用另一个数组变量接受数组并不是拷贝,这两个数组变量实际上引用了同一个数组对象。

正确的做法是,使用Arrays类的静态方法

//不赋值初始全为0
int[] numbers = new int[10];
int[] copiedArray = Arrays.copyOf(numbers, numbers.length);

第二个参数是新数组的长度,如果小于原数组,只拷贝原数组前面的一部分,如果大于原数组,后面的部分给予默认值。

数组排序

调用默认的快速排序方法完成从小到大的排序

Arrays.sort(array)

Arrays类的其他方法

方法

返回类型

描述

copyOfRange(xxx[] a, int start, int end)

xxx[]

复制原数组某一区间段的值, end可以超过原数组的长度,超出的部分给予默认值

binarySearch(xxx[] a, xxx v)、binarySearch(xxx[] a, int start, int end, xxx v)

int

使用二分查找返回查找值的位置,否则返回一个负数值r,-r-1是该元素应该插入到数组中的位置

fill(xxx[] a, xxx v)

void

用一个值把数组全覆盖

equals(xxx[] a, xxx[] b)

boolean

两数组是否相等

deepToString(xxx[] a [])

String

用于打印一个多维数组

toString(xxx[] a)

String

打印一个一维数组

asList(T... a)

List

将可变长数组中的元素转换为ArrayList,这个ArrayList是Arrays的成员内部类