Java 基础

Java 编程基础知识(概述)

Java 的数据类型

Java 中共有八种基本数据类型,其中有 4 种整形、2 种浮点类型、1 种字符串类型和 1 种布尔类型。

整形

整形用于表示没有小数位的数值,允许为复数。Java 共有四种整形。

类型储存要求取值范围
int4 字节-2147483648~2147483647
short2 字节-32768~32767
long8 字节-9223372036854775808~9223372036854775807
byte1 字节-128~127

其中长整形必须有一个后缀 L 或 l。

十六进制数值有一个前缀 0x 或 0X。

八进制有一个前缀 0(相对比较容易混淆)

从 Java7 开始前缀为 0b 或 0B 可以表示二进制数。还可以为数字添加下划线(编译器会自动去除)如 1_000_000。

浮点类型

浮点数用来表示有小数部分的数值。

类型储存需求取值范围
float4 字节大约$\pm$3.40282347E+38F(有效位数为 6~7 位)
double8 字节大约$\pm$1.79769313486231570E+308(有效位为 25 位)

double 表示的数值精度是 float 类型的两倍。

float 类型的数值结尾要有一个后缀 F 或 f。没有后缀 F 的浮点数值总是默认为 double 类型。也可以在浮点数值后面加 D 或者 d。

:::info

可以使用十六进制来表示浮点数值,如:$2^{-3}$可以表示成 0x1.0p*3。

在十六进制表示法中,使用 p 代表指数,而不是 e。(e 是一个十六进制数位)

在使用十六进制来表示的时候指数采用十进制。

:::

字符类型

char 类型原本用于表达单个字符。不过现在有些 Unicode 字符可以用一个 char 值来描述,有些则需要两个 char 值。

类型储存需求取值范围
char2 字节大约 Unicode o ~ Unicode $2^{16}-1$

char 类型的字面常量要用单引号括起来。例如:’A’是编码值为 65 的字符常量。它与”A”不同,”A”是包含一个字符 A 的字符串。char 类型的值可以表示为十六进制值,其范围是\u0000 到\uFFFF。

常见转义字符的转义序列

转义序列名称Unicode 值
\b退格\u0008
\t制表\u0009
\n换行\u000a
\r回车\u000d
"双引号\u0022
'单引号\u0027
\反斜杠\u005c

:::warning

Unicode 转义序列会在解析代码之前得到处理。例如”\u0022+\u0022”并不是一个由引号(U+0022)包围加号构成的字符串。实际上,\u0022 会在解析之前转换为”,这回得到””+””,也就是一个空串。

:::

布尔类型

boolean 有两个值:false 和 true,用于判定逻辑条件。与 C 语言不同,整数类型和布尔类型不能相互转换。

类型储存需求取值范围
boolean-true/false

自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来。

拆箱:将包装类型转换为基本数据类型。

1
2
3
4
5
// 在JavaSE5之前,如果想要声明一个数值为10的Integer对象,必须:
Integer i = new Integer(10);
// 而在JavaSE5开始,如果生成一个数值为10的Integer对象,只需要:
Integer i = 10; // 装箱
int n = i; // 拆箱

简单来说,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。

常见包装器类型
数据类型包装器
int(4 字节)Integer
byte(1 字节)Byte
short(2 字节)Short
long(8 字节)Long
float(4 字节)Float
double(8 字节)Double
char(2 字节)Character
boolean(未定)Boolean
数值类型自动转换

占空间小的数据类型可以向比其占空间大的的数据类型进行自动转换。

操作数 1 类型操作数 2 类型转换后的类型
byte、short、charintint
byte、short、char、intlonglong
byte、short、char、int、longfloatfloat
byte、short、char、int、long、floatdoubledouble
数值类型强制转换

有时我们也会将占空间大的数据类型转换为小的数据类型。这么做会导致数据截断或产生一个完全不同的值:

1
2
3
double x = 9.999;
int nx = x;
// nx = 9;

变量

变量的声明

类型 变量名;

变量名必须以字母开头并以数字或数字构成,与其他语言不同,Java 的字母的范围更大。

Java 的字母包括”A”-“Z”、”a”-“z”、++”_“++、++”$”++,也可以使用任何语言的标准 Unicode 字符。

尽管$是一个合法的 Java 字符,但普通用户不能在自己的代码中使用,因为它只能用于 Java 编译器或其他工具生成的名字中。

Java9 及之前的版本不允许使用_作为变量名。

Java 关键字也不允许作为变量名。

变量的初始化

声明一个变量之后,必须用复制语句对变量进行显式初始化,不可以使用未初始化的值。

1
2
3
4
5
# 方式一
int count = 1;
# 方式二
int num;
num = 2;

原则上,变量的声明可以写在任何地方,但在实际开发中最好将变量尽可能近的声明在离第一次使用的地方附近。

:::info

从 Java10 开始,对于++局部变量,如果可以从它的初始值推断出它的类型,则就不再需要声明类型。只需要使用关键字 var 而无需使用类型。(不推荐使用,因为在日后维护的时候程序员需要自行判断类型。)

:::

常量

在 Java 中使用关键字 final 指示常量。

1
final int num = 1;

关键字 final 表示这个变量只能被赋值一次,声明的常量名要大写。

枚举

有时,变量的取值在一个有限的集合内,针对这种情况,可以自定义枚举类型。

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

数组

数组是一种数据结构,用来保存同一类型值的集合。通过一个整型下标(索引)来访问数组中的每一个值。

++数组是不可变的,可变的是数组列表(ArrayList)++

1
2
3
4
5
6
7
// Java式声明
// 初始化
int[] a = {1,2,3,4,5,6};
// 匿名声明
new int[]{1,2,3,4,5};
// C式声明
int a[];

:::info

Java 允许数组长度为 0。

:::

数组拷贝

浅拷贝

Java 中允许将一个数组变量拷贝到另一个数组变量。这时两个变量将引用同一个数组。

1
2
3
4
5
int b[] = {1,2,3,4,5};
int a[] = b;
a[2] = 2;
System.out.println(a[2]); // 2
System.out.println(b[2]); // 2
深拷贝

如果想要将一个数组拷贝到一个新的数组中可以使用 copyOf 方法。如果数组元素是数值型,那么额外的元素将被赋值为 0;如果数组元素是布尔型,则将赋值为 false。相反,如果小于原始数组的长度,则值拷贝前面的值。

1
2
3
4
5
int[] b = {1,2,3,4,5};
int[] a = Arrays.copyOf(b,b.length);
a[2] = 2;
System.out.println(a[2]); // 2
System.out.println(b[2]); // 3

字符串

从概念上讲,Java 字符串就是 Unicode 字符序列。Java 没有内置的字符串类型,但是在 Java 类库中提供了一个预定义类:String。每个用””括起来的字符串就是字符串。

连接

Java 允许使用+连接两个字符串。

不可变字符串

++String 类没有提供修改字符串中某个字符的方法。如果需要修改 String 类型字符串的值,只能重新赋值(引用),不能在原串上进行修改。++

不可变字符串的优点:编译器可以让字符串共享。

Java 的设计者认为共享带来的高效率远高于提取子串、拼接字符串。因为在大多数情况下,我们都不会修改字符串。

可变字符串:StringBuilder 和 StringBuffer

可变性

简单来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final byte[] value;,所以 String 对象是不可变的。

:::info

Java9 之后 String、StringBuffer、StringBuilder 的实现使用 byte 数组保存,之前使用 char 数组。

:::

而 StringBuilder 和 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中使用byte[] value;来保存字符串但是没有使用 final 来修饰,所以这两种对象是可变的。

线程安全性

String 中的对象是不可变的,可以理解为常量,线程安全。

StringBuffer 中的自定义方法都加了同步锁synchronized,所以是线程安全的。

StringBuilder 中的自定义方法没加同步锁,所以是线程不安全的。

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。、StringBuffer 和 StringBuilder 每次都会对其本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 仅比使用 StringBuffer 快 10%-15%但却要冒多线程不安全的风险。

总结
  1. 操作量少的数据:使用 String
  2. 单线程操作字符串缓冲区下大数据:StringBuilder
  3. 多线程操作字符串缓冲区下大数据:StringBuffer

空串与 NULL 值

空串””是长度为 0 的字符。空串是一个 Java 对象,有自己的串长度(0)和内容(空)。String 变量还可以放一个特殊的值,名为 null,表示目前没有任何对象与该变量关联。

检测字符串是否相等

==

它的作用是判断两个对象的地址是否相等,即判断两个对象是不是一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)

因为 Java 只有值传递,所以,对于==来说,不管是比较基本数据类型,还是应用数据类型的变量,其本质比较的都是值,只是引用类型变量存的是对象的地址。

equals

它的作用也是判断两个对象是否相等,他++不能用于比较基本数据类型的变量++。equals 方法存在于 Object 类中,而 Object 类是所有类的直接或间接父类。

1
2
3
4
// equals() 源码
public boolean equals(Object obj) {
return (this == obj);
}

equals()方法存在两种使用情况:

  • 情况 1:类没有重写 equals 方法。则通过 equals 比较该类的两个对象时,等价于通过”==”比较这两个对象。使用的是 Object 类的 equals 方法。

  • 情况 2:类重写了 equals 方法。一般,我们都重写 equals 方法来判断两个对象的内容相等;若他们的内容相等。则返回 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为一个引用
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 同上
if (aa == bb){
System.out.println("aa == bb"); // true
}
if (a == b){
System.out.println("a == b");
}
if (a.equals(b)){
System.out.println("aEQb"); // aEQb
}
if (42 == 42.0){
System.out.println("true"); // true
}
}
}

说明:

  • String 中的 equals 是被重写过的
1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}

因为 Object 的 equals 方法比较的是对象的内存地址,而 String 的 equals 比较的是对象的值。

  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

hashCode

hashCode 方法的作用是获取哈希码,也称散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()也定义在 Object 类中,这就意味着 Java 中的任何类都包含 hashCode 函数。散列表中存储的是键值对(key-value),它的特点是:能根据”键”快速的检索出对应的”值”。这其中就利用到了散列码。

:::info

Object 的 hashCode 方法是个本地方法,是使用 c 语言或 c++实现的,该方法通常用来将对象的内存地址转换为整数之后返回。

:::

当我们将对象存入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 HashCode 值进行比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就打打减少了 equals 的次数,响应就打打提高了执行速度。

在重写 equals 方法时必须重写 hashCode 方法。因为如果两个对象相等,则 hashCode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashCode 值,他们也不一定是相等的。因此,equals 方法被重写,hashCode 必须被重写。

泛型

Java 泛型是 JDK5 中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是所操作的数据类型被指定为一个参数。

:::info

Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说的类型擦除。

:::

泛型通配符(约定俗成)

? 表示不确定的 Java 类型

T(type) 表示具体的 Java 类型

K V(key value) 分别表示 Java 键值中的 Key、Value

E(element) 代表元素

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Generics<T> {
private T key;

public Generics(T key) {
this.key = key;
}

public T getKey() {
return key;
}
}

// 实例化
public static void main(String[] args) {
System.out.println(new Generics<Integer>(123).getKey());
}

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface Generator<T>{
public T method();
}

// 实现泛型接口,不指定类型
public class GeneratorImpl<T> implements Generator<T> {
@Override
public T method() {
return null;
}
}

// 实现泛型接口,指定类型
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public static <E> void printArrays(E[] inputArray) {
for (E e : inputArray) {
System.out.println(e);
}
}
// 使用
public static void main(String[] args) {
// 这里不能使用int,因为int是基本数据类型。
Integer[] intArray = {1, 2, 3};
String[] stringArray = {"Hello", "World"};
GeneratorImpl.printArrays(intArray);
GeneratorImpl.printArrays(stringArray);
}

类型擦除

运算符

算数运算符

+、-、*、/、%

当参与/运算的两个操作数都是整数时表示整数除法,否则表示浮点数。

结合赋值运算符

1
2
x += 4;
x = x + 4;

自增自减

1
2
int n = 1;
n++;

前缀形式是先增减后运算

后缀形式是先运算后增减

关系运算符

操作符功能
==判等
!=不等
&&(左条件不满足时会截断(短路))
||(左条件满足时会截断(短路))
condition ? expression1 : expression2三元表达式

位运算符

处理整形类时可以直接对组成整数的各个位进行操作。这意味着可以使用掩码技术得到整数中的各个位。

位运算包括:&(and)、|(or)、^(xor)、`(not)

:::info

用于布尔值上时&和|运算符也会得到一个布尔值。与&&和||不同的是&和|不会短路。

:::

位模式左移与位模式右移:<< 、>> 、>>>位模式右移会用 0 填充高位,++没有<<<

输入输出

输入

  • 方式一
1
2
3
4
5
6
7
8
9
// 构建Scanner对象
Scanner in = new Scanner(System.in);
// 接受空格
String name = in.nextLine();
// 以空格作为分隔
String name = in.next();
// 读取一个整数
int num = in.nextInt();
// ...
  • 方式二
1
2
       BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String s = bufferedReader.readLine();

输出

1
2
3
4
5
6
// 不换行输出
System.out.print("");
// 换行输出
System.out.println("");
// 与C语言相同的格式化输出
System.out.printf();

流程控制

块中语句只有一行时可以不加{}。

条件语句

1
2
3
4
5
6
7
if (condition){

}else if(condition1){

}else{

};

循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// while循环形式1
while(condition){

};

// while循环形式2
do {

}while(condition);

// for循环demo
for(int i = 1;i <= 10;i++){

}

// for each循环
for variable in collection:
statement;

多重选择

1
2
3
4
5
6
7
8
9
10
11
switch (choice){
case choice1:
...;
break;
case choice2:
...;
break;
default:
...;
break;
}

其中 case 的标签可以是:

  • 类型为 char、byte、short 或 int 的常量表达式;
  • 枚举常量;
  • 从 java7 开始还可以是字符串字面量;

对象与类

面对对象三大特性

封装

封装指把一个对象的状态信息(属性)隐藏在对象内部,不允许外部变量直接访问对象的内部信息。但是可以提供一些可以被外接访问的方法来操作属性。

继承

不同类型的对象,相互之间经常有一定数量的共同点。同时每一个对象还定义了额外的特性使他们与众不同。继承是使用已存在的类的定义为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速的创建新的类,可以提高代码的重用,程序的可维护性,节约大量创建新类的时间,提高我们的开发效率。

  1. 子类++拥有++父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是++无法访问++的。
  2. 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。(重写)

多态

表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。

多态的特点
  • 对象类型和引用类型之间具有继承/实现的关系;
  • 引用类型变量发出的方法调用的是到底哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用”只在子类存在但父类不存在的方法”;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

构造器

构造器与类同名。在构造一个类的对象时,构造器会执行,从而将实例字段初始化为所希望的状态。

构造器与其他方法有一个重要的不同。构造器总是结合 new 运算符来调用。不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的。

  • 构造器与类同名
  • 每个类可以有一个以上的构造器
  • 构造器可以有 0 个或更多参数
  • 构造器没有返回值,但不能用 void 声明构造函数
  • 构造器总是伴随着 new 操作符一起调用
  • 生成类的对象时自动执行,无需调用
  • 构造器不可以被重写,只能被重载

:::info

所有的对象都是在堆中构造的

:::

隐式参数与显式参数

在一个方法中,关键字 this 指隐式参数。一个方法可以访问所有所属类的所有对象的属性。每个方法都会隐式的传入 this。方法中明确写出的形式参数就是显式传递的参数。

访问修饰符

  1. public:对外部完全可见
  2. protected:对本包和所有子类可见
  3. 默认(无需修饰符):对本包可见
  4. private:仅对本类可见

Static

如果将一个字段/方法定义为 static,每一个类只有一个这样的字段。

而对于非静态字段的实例字段,每个对象都有一个自己的一个副本。

工厂方法

使用静态工厂方法来构建对象。例如 LocalDate.now()和 NumberFormat.of()。

抽象类

很多时候我们定义父类并不是为了使用这个类,而仅仅式为了使用子类将其拓展。在这种情况下我们使用抽象类

抽象类的可以拥有抽象方法和普通方法。

抽象方法默认没有方法体。

向上转型的情况下可以以子类创建抽象类的对象变量。但是他仅能引用非抽象子类的对象。

如果子类没有重写全部的抽象方法则子类必须也为抽象。

接口

接口不是类,而是对类的一组需求描述,这些类要遵循接口描述的统一格式进行定义。

接口中的方法默认都是 public 的。实现接口时也必须把接口声明为 public 的。

接口内能声明变量,并且接口中的变量默认都是静态的。

接口可以通过 default 关键字和 static 关键字使接口拥有方法体。虽然在接口中可以有静态方法,但是这有违使用接口的初衷,所以不建议在接口中使用静态方法。

与抽象类相比,接口可以继承多个接口(extends interface1,interface2)。一个类也可以实现多个接口(implements interface1,interface2)

与抽象类相同,接口也可以引用子类实例。

接口中如果定义了默认方法,然后又在父类或者其他接口中定义了相同的方法 Java 会遵循一下规则:

  • 父类优先:如果父类提供了一个具体方法,同名同参的默认方法将会被忽略。
  • 接口冲突:如果一个父接口提供了一个默认方法,必须重写这个方法来解决冲突。

本文章参考列表:

《Head First Java》第二版

《 Java 核心技术卷一》第十一版

JavaGuide]: “https://snailclimb.gitee.io/javaguide"

深入剖析 Java 中的装箱和拆箱]:”https://www.cnblogs.com/dolphin0520/p/3780005.html"