PriorityQueue
-
构造方法:
PriorityQueue()
:创建一个空的优先队列,其元素按照元素的自然顺序进行排序。PriorityQueue(Collection<? extends E> c)
:创建一个优先队列,包含给定集合中的元素,按照元素的自然顺序进行排序。PriorityQueue(int initialCapacity)
:创建一个空的优先队列,其初始容量为initialCapacity
,按照元素的自然顺序进行排序。PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
:创建一个空的优先队列,其初始容量为initialCapacity
,按照提供的Comparator
进行排序。
-
添加元素:
boolean add(E e)
:将元素e
添加到队列中,如果队列容量不足,会自动扩容。boolean offer(E e)
:将元素e
添加到队列中,如果队列容量不足,返回false
。
-
移除元素:
E poll()
:移除并返回队列中优先级最高的元素(队列的头部元素),如果队列为空,则返回null
。E remove()
:移除并返回队列中优先级最高的元素,如果队列为空,则抛出NoSuchElementException
。
-
E peek()
:返回队列中优先级最高的元素,但不移除它,如果队列为空,则返回null
。 -
int size()
:返回队列中的元素数量 -
boolean contains(Object o)
:检查队列是否包含指定的元素。 -
boolean isEmpty()
:检查队列是否为空。 -
void clear()
:移除队列中的所有元素。
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0])); 小顶堆
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(b[0], a[0])); 大顶堆
Deque
接口的一些常用方法:
-
插入元素:
void addFirst(E e)
:在队列的头部插入一个元素。void addLast(E e)
:在队列的尾部插入一个元素。boolean offerFirst(E e)
:在队列头部插入一个元素,如果成功返回true
,如果失败(例如队列容量限制)返回false
。boolean offerLast(E e)
:在队列尾部插入一个元素,如果成功返回true
,如果失败返回false
。
-
移除元素:
E removeFirst()
:移除并返回队列头部的元素,如果队列为空则抛出NoSuchElementException
。E removeLast()
:移除并返回队列尾部的元素,如果队列为空则抛出NoSuchElementException
。E pollFirst()
:移除并返回队列头部的元素,如果队列为空则返回null
。E pollLast()
:移除并返回队列尾部的元素,如果队列为空则返回null
。
-
查看元素:
E getFirst()
:返回队列头部的元素,如果队列为空则抛出NoSuchElementException
。E getLast()
:返回队列尾部的元素,如果队列为空则抛出NoSuchElementException
。E peekFirst()
:返回队列头部的元素,如果队列为空则返回null
。E peekLast()
:返回队列尾部的元素,如果队列为空则返回null
。
-
int size()
:返回队列中的元素数量。
// 对字符串进行编码
String hash(String str) {
int[] count = new int[26];
for (int i = 0; i < str.length(); i++) {
count[str.charAt(i) - 'a']++;
}
return Arrays.toString(count); // 只能用这种方式
}
// map的所有values转成list输出
Map<String, List<String>> map = new HashMap<>();
List<List<String>> res = new ArrayList<>(map.values());
// 或者
List<List<String>> result = new ArrayList<>();
result.addAll(map.values());
1、数组
// 数组所有元素初始化为默认值,整型都是0,浮点型是0.0,布尔型是false;
// 数组一旦创建后,大小就不可改变
int m = 5, n = 10;
int[] nums = new int[n]
boolean[][] visited = new boolean[m][n]; // 初始化一个 m * n 的二维布尔数组
2、字符串 String
Java 的字符串处理起来挺麻烦的,因为它不支持用 []
直接访问其中的字符,而且不能直接修改,要转化成 char[]
类型才能修改。
String s1 = "hello world";
char c = s1.charAt(2); // 获取 s1[2] 那个字符 l
char[] chars = s1.toCharArray();
chars[1] = 'a';
String s2 = new String(chars);
System.out.println(s2); // 输出:hallo world
// 注意,一定要用 equals 方法判断字符串是否相同 不要用 == 比较
if (s1.equals(s2)) {
// s1 和 s2 相同
} else {
// s1 和 s2 不相同
}
String s3 = s1 + "!"; // 字符串可以用加号进行拼接
System.out.println(s3); // 输出:hello world!
String.join(".", chars)
str.substring(i, j); // 截取字符串 不包含j
Java 的字符串不能直接修改,要用 toCharArray
转化成 char[]
的数组进行修改,然后再转换回 String
类型。
虽然字符串支持用 +
进行拼接,但是效率并不高,并不建议在 for 循环中使用。如果需要进行频繁的字符串拼接,推荐使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (char c = 'a'; c <= 'f'; c++) {
sb.append(c);
}
sb.append('g').append("hij").append(123); // append 方法支持拼接字符、字符串、数字等类型
String res = sb.toString();
System.out.println(res); // 输出:abcdefghij123
sb.delete(n, sb.length());
sb.deleteCharAt(sb.length() - 1);
"Hello".contains("ll"); // true 是否包含子串:
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
"Hello".substring(2); // "llo" 提取子串
"Hello".substring(2, 4); "ll" 提取子串 注意索引号是从0开始的。
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D" 通过正则表达式替换
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"} 要分割字符串,使用split()方法,并且传入的也是正则表达式
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C" 拼接字符串使用静态方法join(),它用指定的字符串连接字符串数组
// 要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()。这是一个重载方法,编译器会根据参数自动选择合适的方法
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
// 要把字符串转换为其他类型,就需要根据情况。例如,把字符串转换为int类型
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
// 把字符串转换为boolean类型
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
// 要特别注意,Integer有个getInteger(String)方法,它不是将字符串转换为int,而是把该字符串对应的系统变量转换为Integer
Integer.getInteger("java.version"); // 版本号,11
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String String和char[]类型可以互相转换
3、动态数组 ArrayList
ArrayList
相当于把 Java 内置的数组类型做了包装,初始化方法:
ArrayList<String> strings = new ArrayList<>(); // 初始化一个存储 String 类型的动态数组
ArrayList<Integer> nums = new ArrayList<>(); // 初始化一个存储 int 类型的动态数组
常用的方法如下(E
代表元素类型):
isEmpty() // 判断数组是否为空
size() // 返回数组的元素个数
get(int index) // 返回索引 index 的元素
add() // 在数组尾部添加元素 e
addAll() // 添加集合中的所有元素到 arraylist 中
contains() // 判断元素是否在 arraylist
indexOf() // 返回指定元素的索引值, 如果 obj 元素在动态数组中重复出现,返回第一个索引值, 如果不存在指定的元素 -1
remove() // 删除 arraylist 里的单个元素
subList(i, j) // 截取部分 arraylist 的元素 不包含 j位置
set(int index, E element) // 在 index 位置替换进去的新元素
sort() // 不返回任何值,只更改动态数组列表中元素的顺序
toArray() // 将 arraylist 转换为数组
4、双链表 LinkedList
ArrayList
列表底层是数组实现的,而 LinkedList
底层是双链表实现的,初始化方法也是类似的:
LinkedList<Integer> nums = new LinkedList<>(); // 初始化一个存储 int 类型的双链表
LinkedList<String> strings = new LinkedList<>(); // 初始化一个存储 String 类型的双链表
一般算法题中我们会用到以下方法(E
代表元素类型):
boolean isEmpty() // 判断链表是否为空
int size() // 返回链表的元素个数
boolean contains(Object o) // 判断链表中是否存在元素 o
boolean add(E e) // 在链表尾部添加元素 e
void addLast(E e) // 在链表尾部添加元素 e
void addFirst(E e) // 在链表头部添加元素 e
E removeFirst() // 删除链表头部第一个元素
E removeLast() // 删除链表尾部最后一个元素
这些也都是最简单的方法,和 ArrayList
不同的是,我们更多地使用了 LinkedList
对于头部和尾部元素的操作,因为底层数据结构为链表,直接操作头尾的元素效率较高。其中只有 contains
方法的时间复杂度是 O(N)
,因为必须遍历整个链表才能判断元素是否存在。
5、哈希表 HashMap
HashMap<Integer, String> map = new HashMap<>(); // 整数映射到字符串的哈希表
HashMap<String, int[]> map = new HashMap<>(); // 字符串映射到数组的哈希表
boolean containsKey(Object key) // 判断哈希表中是否存在键 key
V get(Object key) // 获得键 key 对应的值,若 key 不存在,则返回 null
V put(K key, V value) // 将 key, value 键值对存入哈希表
V remove(Object key) // 如果 key 存在,删除 key 并返回对应的值
map.put(key, map.getOrDefault(key, 0) + 1); // HashMap操作
6、哈希集合 HashSet
Set<String> set = new HashSet<>(); // 新建一个存储 String 的哈希集合
boolean add(E e) // 如果 e 不存在,则添加 e 到哈希集合
boolean contains(Object o) // 判断元素 o 是否存在于哈希集合中
boolean remove(Object o) // 如果元素 o 存在,在删除元素 o
7、队列 Queue
我们可能在算法题中用到的方法(E
表示元素类型):与之前的数据结构不同,Queue是一个接口(Interface),所以它的初始化方法有些特别
// 新建一个存储String的队列
Queue<String> q = new LinkedList<>();
boolean isEmpty() // 判断队列是否为空
int size() // 返回队列中元素的个数
E peek() // 返回对头的元素
boolean offer(E e) // 将元素e插入队尾
E poll() // 删除并返回对头的元素
8、堆栈 Stack
// 新建一个存储String的队列
Stack<String> s = new Stack<>();
boolean isEmpty() // 判断堆栈是否为空
int size() // 返回栈中元素的个数
E peek() // 返回栈顶的元素
E push() // 将元素压入栈顶
E pop() // 删除并返回栈顶的元素
常用工具类
Math.abs(-100); // 100 求绝对值
Math.max(100, 99); // 100 最大值
Math.min(1.2, 2.3); // 1.2 最小值
Math.pow(2, 10); // 2的10次方=1024 计算xy次方
Math.sqrt(2); // 1.414... 根号x
Math.exp(2); // 7.389... 计算ex次方
Math.log(4); // 1.386... 计算以e为底的对数
Math.log10(100); // 2 计算以10为底的对数
Math.sin(3.14); // 0.00159...
Math.cos(3.14); // -0.9999...
Math.tan(3.14); // -0.0015...
double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.7182818...
Math.random(); // 0.53907... 每次都不一样 生成一个随机数x,x的范围是0 <= x < 1
// ++n表示先加1再引用n,n++表示先引用n再加1
// b ? x : y; 三元运算符,根据第一个布尔表达式的结果,分别返回后续两个表达式之一的计算结果。
int[] ns = { 68, 79, 91, 85, 62 };// 整型数组初始化
String[] names = {"ABC", "XYZ", "zoo"}; // 字符串数组初始化
Arrays.copyOfRange(数组, i, j) // 不包含 j
Arrays.toString(tmp); // int[] 转 String
Character.isLetter() // 是否是一个字母
Character.isDigit() // 是否是一个数字字符
Character.isLetterOrDigit(s.charAt(left)) // 判断是否是字母或字符串
Character.isLowerCase() // 是否是小写字母
Character.isUpperCase() // 是否是大写字母
Character.toLowerCase(s.charAt(left)) // 字母小写
Character.toUpperCase() // 指定字母的大写形式
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names); // 要高效拼接字符串数组
// 一个完整的Java程序的基本结构:关键字static是一个修饰符,它表示静态方法,
// Java入口程序规定的方法必须是静态方法,方法名必须为main,括号内的参数必须是String数组
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
===============================================================================
基本数据类型
基本数据类型是CPU可以直接进行运算的类型:
-
整数类型:byte,short,int,long
-
浮点数类型:float,double
-
字符类型:char
-
布尔类型:boolean
不同的数据类型占用的字节数不一样。我们看一下Java基本数据类型占用的字节数:
┌───┐
byte │ │
└───┘
┌───┬───┐
short │ │ │
└───┴───┘
┌───┬───┬───┬───┐
int │ │ │ │ │
└───┴───┴───┴───┘
┌───┬───┬───┬───┬───┬───┬───┬───┐
long │ │ │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
┌───┬───┬───┬───┐
float │ │ │ │ │
└───┴───┴───┴───┘
┌───┬───┬───┬───┬───┬───┬───┬───┐
double │ │ │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
┌───┬───┐
char │ │ │
└───┴───┘
byte
恰好就是一个字节,而long
和double
需要8个字节。
整型
对于整型类型,Java只定义了带符号的整型,因此,最高位的bit表示符号位(0表示正数,1表示负数)。各种整型能表示的最大范围如下:
- byte:-128 ~ 127
- short: -32768 ~ 32767
- int: -2147483648 ~ 2147483647
- long: -9223372036854775808 ~ 9223372036854775807
浮点型
浮点类型的数就是小数,因为小数用科学计数法表示的时候,小数点是可以“浮动”的,如1234.5可以表示成12.345x102,也可以表示成1.2345x103,所以称为浮点数。
下面是定义浮点数的例子:
float f1 = 3.14f;
float f2 = 3.14e38f; // 科学计数法表示的3.14x10^38
float f3 = 1.0; // 错误:不带f结尾的是double类型,不能赋值给float
double d = 1.79e308;
double d2 = -1.79e308;
double d3 = 4.9e-324; // 科学计数法表示的4.9x10^-324
对于float
类型,需要加上f
后缀。
浮点数可表示的范围非常大,float
类型可最大表示3.4x1038,而double
类型可最大表示1.79x10308。
布尔类型
布尔类型boolean
只有true
和false
两个值,布尔类型总是关系运算的计算结果:
boolean b1 = true;
boolean b2 = false;
boolean isGreater = 5 > 3; // 计算结果为true
int age = 12;
boolean isAdult = age >= 18; // 计算结果为false
Java语言对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要1 bit,但是通常JVM内部会把boolean
表示为4字节整数。
字符类型
字符类型char
表示一个字符。Java的char
类型除了可表示标准的ASCII外,还可以表示一个Unicode字符:
注意char
类型使用单引号'
,且仅有一个字符,要和双引号"
的字符串类型区分开。
引用类型
除了上述基本类型的变量,剩下的都是引用类型。例如,引用类型最常用的就是String
字符串:
String s = "hello";
引用类型的变量类似于C语言的指针,它内部存储一个“地址”,指向某个对象在内存的位置。
常量
定义变量的时候,如果加上final
修饰符,这个变量就变成了常量:
final double PI = 3.14; // PI是一个常量
double r = 5.0;
double area = PI * r * r;
PI = 300; // compile error!
常量在定义时进行初始化后就不可再次赋值,再次赋值会导致编译错误。
常量的作用是用有意义的变量名来避免魔术数字(Magic number),例如,不要在代码中到处写3.14
,而是定义一个常量。如果将来需要提高计算精度,我们只需要在常量的定义处修改,例如,改成3.1416
,而不必在所有地方替换3.14
。
根据习惯,常量名通常全部大写。
var关键字
有些时候,类型的名字太长,写起来比较麻烦。例如:
StringBuilder sb = new StringBuilder();
这个时候,如果想省略变量类型,可以使用var
关键字:
var sb = new StringBuilder();
编译器会根据赋值语句自动推断出变量sb
的类型是StringBuilder
。对编译器来说,语句:
var sb = new StringBuilder();
实际上会自动变成:
StringBuilder sb = new StringBuilder();
因此,使用var
定义变量,仅仅是少写了变量类型而已。
变量的作用范围
在Java中,多行语句用{ }括起来。很多控制语句,例如条件判断和循环,都以{ }作为它们自身的范围,例如:
if (...) { // if开始
...
while (...) { // while 开始
...
if (...) { // if开始
...
} // if结束
...
} // while结束
...
} // if结束
只要正确地嵌套这些{ },编译器就能识别出语句块的开始和结束。而在语句块中定义的变量,它有一个作用域,就是从定义处开始,到语句块结束。超出了作用域引用这些变量,编译器会报错。举个例子:
{
...
int i = 0; // 变量i从这里开始定义
...
{
...
int x = 1; // 变量x从这里开始定义
...
{
...
String s = "hello"; // 变量s从这里开始定义
...
} // 变量s作用域到此结束
...
// 注意,这是一个新的变量s,它和上面的变量同名,
// 但是因为作用域不同,它们是两个不同的变量:
String s = "hi";
...
} // 变量x和s作用域到此结束
...
} // 变量i作用域到此结束
定义变量时,要遵循作用域最小化原则,尽量将变量定义在尽可能小的作用域,并且,不要重复使用变量名。
溢出
要特别注意,整数由于存在范围限制,如果计算结果超出了范围,就会产生溢出,而溢出不会出错,却会得到一个奇怪的结果:
可以把int
换成long
类型,由于long
可表示的整型范围更大,所以结果就不会溢出:
long x = 2147483640;
long y = 15;
long sum = x + y;
System.out.println(sum); // 2147483655
还有一种简写的运算符,即+=
,-=
,*=
,/=
,它们的使用方法如下:
n += 100; // 3409, 相当于 n = n + 100;
n -= 100; // 3309, 相当于 n = n - 100;
移位运算
在计算机中,整数总是以二进制的形式表示。例如,int
类型的整数7
使用4字节表示的二进制如下:
00000000 0000000 0000000 00000111
可以对整数进行移位运算。对整数7
左移1位将得到整数14
,左移两位将得到整数28
:
int n = 7; // 00000000 00000000 00000000 00000111 = 7
int a = n << 1; // 00000000 00000000 00000000 00001110 = 14
int b = n << 2; // 00000000 00000000 00000000 00011100 = 28
int c = n << 28; // 01110000 00000000 00000000 00000000 = 1879048192
int d = n << 29; // 11100000 00000000 00000000 00000000 = -536870912
左移29位时,由于最高位变成1
,因此结果变成了负数。
类似的,对整数28进行右移,结果如下:
int n = 7; // 00000000 00000000 00000000 00000111 = 7
int a = n >> 1; // 00000000 00000000 00000000 00000011 = 3
int b = n >> 2; // 00000000 00000000 00000000 00000001 = 1
int c = n >> 3; // 00000000 00000000 00000000 00000000 = 0
如果对一个负数进行右移,最高位的1
不动,结果仍然是一个负数:
int n = -536870912;
int a = n >> 1; // 11110000 00000000 00000000 00000000 = -268435456
int b = n >> 2; // 11111000 00000000 00000000 00000000 = -134217728
int c = n >> 28; // 11111111 11111111 11111111 11111110 = -2
int d = n >> 29; // 11111111 11111111 11111111 11111111 = -1
还有一种无符号的右移运算,使用>>>
,它的特点是不管符号位,右移后高位总是补0
,因此,对一个负数进行>>>
右移,它会变成正数,原因是最高位的1
变成了0
:
int n = -536870912;
int a = n >>> 1; // 01110000 00000000 00000000 00000000 = 1879048192
int b = n >>> 2; // 00111000 00000000 00000000 00000000 = 939524096
int c = n >>> 29; // 00000000 00000000 00000000 00000111 = 7
int d = n >>> 31; // 00000000 00000000 00000000 00000001 = 1
对byte
和short
类型进行移位时,会首先转换为int
再进行位移。
仔细观察可发现,左移实际上就是不断地×2,右移实际上就是不断地÷2。
位运算
位运算是按位进行与、或、非和异或的运算。
与运算的规则是,必须两个数同时为1
,结果才为1
:
n = 0 & 0; // 0
n = 0 & 1; // 0
n = 1 & 0; // 0
n = 1 & 1; // 1
或运算的规则是,只要任意一个为1
,结果就为1
:
n = 0 | 0; // 0
n = 0 | 1; // 1
n = 1 | 0; // 1
n = 1 | 1; // 1
非运算的规则是,0
和1
互换:
n = ~0; // 1
n = ~1; // 0
异或运算的规则是,如果两个数不同,结果为1
,否则为0
:
n = 0 ^ 0; // 0
n = 0 ^ 1; // 1
n = 1 ^ 0; // 1
n = 1 ^ 1; // 0
对两个整数进行位运算,实际上就是按位对齐,然后依次对每一位进行运算。
运算优先级
在Java的计算表达式中,运算优先级从高到低依次是:
()
!
~
++
--
*
/
%
+
-
<<
>>
>>>
&
|
+=
-=
*=
/=
记不住也没关系,只需要加括号就可以保证运算的优先级正确。
类型自动提升与强制转型
在运算过程中,如果参与运算的两个数类型不一致,那么计算结果为较大类型的整型。例如,short
和int
计算,结果总是int
,原因是short
首先自动被转型为int
:
也可以将结果强制转型,即将大范围的整数转型为小范围的整数。强制转型使用(类型)
,例如,将int
强制转型为short
:
int i = 12345;
short s = (short) i; // 12345
要注意,超出范围的强制转型会得到错误的结果,原因是转型时,int
的两个高位字节直接被扔掉,仅保留了低位的两个字节
浮点数运算
浮点数运算和整数运算相比,只能进行加减乘除这些数值计算,不能做位运算和移位运算。
在计算机中,浮点数虽然表示的范围大,但是,浮点数有个非常重要的特点,就是浮点数常常无法精确表示。
举个栗子:
浮点数0.1
在计算机中就无法精确表示,因为十进制的0.1
换算成二进制是一个无限循环小数,很显然,无论使用float
还是double
,都只能存储一个0.1
的近似值。但是,0.5
这个浮点数又可以精确地表示。
因为浮点数常常无法精确表示,因此,浮点数运算会产生误差:
由于浮点数存在运算误差,所以比较两个浮点数是否相等常常会出现错误的结果。正确的比较方法是判断两个浮点数之差的绝对值是否小于一个很小的数:
// 比较x和y是否相等,先计算其差的绝对值:
double r = Math.abs(x - y);
// 再判断绝对值是否足够小:
if (r < 0.00001) {
// 可以认为相等
} else {
// 不相等
}
浮点数在内存的表示方法和整数比更加复杂。Java的浮点数完全遵循IEEE-754标准,这也是绝大多数计算机平台都支持的浮点数标准表示方法。
类型提升
如果参与运算的两个数其中一个是整型,那么整型可以自动提升到浮点型:
需要特别注意,在一个复杂的四则运算中,两个整数的运算不会出现自动提升的情况。例如:
double d = 1.2 + 24 / 5; // 5.2
计算结果为5.2
,原因是编译器计算24 / 5
这个子表达式时,按两个整数进行运算,结果仍为整数4
。
溢出
整数运算在除数为0
时会报错,而浮点数运算在除数为0
时,不会报错,但会返回几个特殊值:
NaN
表示Not a NumberInfinity
表示无穷大-Infinity
表示负无穷大
例如:
double d1 = 0.0 / 0; // NaN
double d2 = 1.0 / 0; // Infinity
double d3 = -1.0 / 0; // -Infinity
这三种特殊值在实际运算中很少碰到,我们只需要了解即可。
强制转型
可以将浮点数强制转型为整数。在转型时,浮点数的小数部分会被丢掉。如果转型后超过了整型能表示的最大范围,将返回整型的最大值。例如:
int n1 = (int) 12.3; // 12
int n2 = (int) 12.7; // 12
int n2 = (int) -12.7; // -12
int n3 = (int) (12.7 + 0.5); // 13
int n4 = (int) 1.2e20; // 2147483647
如果要进行四舍五入,可以对浮点数加上0.5再强制转型:
字符和字符串
Java中,字符和字符串是两个不同的类型。
字符类型
字符类型char
是基本数据类型,它是character
的缩写。一个char
保存一个Unicode字符:
char c1 = 'A';
char c2 = '中';
因为Java在内存中总是使用Unicode表示字符,所以,一个英文字符和一个中文字符都用一个char
类型表示,它们都占用两个字节。要显示一个字符的Unicode编码,只需将char
类型直接赋值给int
类型即可:
int n1 = 'A'; // 字母“A”的Unicodde编码是65
int n2 = '中'; // 汉字“中”的Unicode编码是20013
还可以直接用转义字符\u
+Unicode编码来表示一个字符:
// 注意是十六进制:
char c3 = '\u0041'; // 'A',因为十六进制0041 = 十进制65
char c4 = '\u4e2d'; // '中',因为十六进制4e2d = 十进制20013
字符串类型
和char
类型不同,字符串类型String
是引用类型,我们用双引号"..."
表示字符串。一个字符串可以存储0个到任意个字符:
String s = ""; // 空字符串,包含0个字符
String s1 = "A"; // 包含一个字符
String s2 = "ABC"; // 包含3个字符
String s3 = "中文 ABC"; // 包含6个字符,其中有一个空格
因为字符串使用双引号"..."
表示开始和结束,那如果字符串本身恰好包含一个"
字符怎么表示?例如,"abc"xyz"
,编译器就无法判断中间的引号究竟是字符串的一部分还是表示字符串结束。这个时候,我们需要借助转义字符\
:
String s = "abc\"xyz"; // 包含7个字符: a, b, c, ", x, y, z
因为\
是转义字符,所以,两个\\
表示一个\
字符:
String s = "abc\\xyz"; // 包含7个字符: a, b, c, \, x, y, z
常见的转义字符包括:
\"
表示字符"
\'
表示字符'
\\
表示字符\
\n
表示换行符\r
表示回车符\t
表示Tab\u####
表示一个Unicode编码的字符
例如:
String s = "ABC\n\u4e2d\u6587"; // 包含6个字符: A, B, C, 换行符, 中, 文
字符串连接
Java的编译器对字符串做了特殊照顾,可以使用+
连接任意字符串和其他数据类型,这样极大地方便了字符串的处理。如果用+
连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接
String s = s1 + " " + s2 + "!";
String s = "age is " + age;
不可变特性
Java的字符串除了是一个引用类型外,还有个重要特点,就是字符串不可变。考察以下代码:
// 字符串不可变
public class Main {
public static void main(String[] args) {
String s = "hello";
System.out.println(s); // 显示 hello
s = "world";
System.out.println(s); // 显示 world
}
}
s ──────────────┐
│
▼
┌───┬───────────┬───┬───────────┬───┐
│ │ "hello" │ │ "world" │ │
└───┴───────────┴───┴───────────┴───┘
原来的字符串"hello"
还在,只是我们无法通过变量s
访问它而已。因此,字符串的不可变是指字符串内容不可变。至于变量,可以一会指向字符串"hello"
,一会指向字符串"world"
。
空值null
引用类型的变量可以指向一个空值null
,它表示不存在,即该变量不指向任何对象。例如:
String s1 = null; // s1是null
String s2 = s1; // s2也是null
String s3 = ""; // s3指向空字符串,不是null
注意要区分空值null
和空字符串""
,空字符串是一个有效的字符串对象,它不等于null
。
小结
Java的字符类型char
是基本类型,字符串类型String
是引用类型;
基本类型的变量是“持有”某个数值,引用类型的变量是“指向”某个对象;
引用类型的变量可以是空值null
;
要区分空值null
和空字符串""
。
字符串数组
如果数组元素不是基本类型,而是一个引用类型,那么,修改数组元素会有哪些不同?
字符串是引用类型,因此我们先定义一个字符串数组:
String[] names = {"ABC", "XYZ", "zoo"};
对于String[]
类型的数组变量names
,它实际上包含3个元素,但每个元素都指向某个字符串对象:
对names[1]
进行赋值,例如names[1] = "cat";
,效果如下:
数组是同一数据类型的集合,数组一旦创建后,大小就不可变;
可以通过索引访问数组元素,但索引超出范围将报错;
数组元素可以是值类型(如int)或引用类型(如String),但数组本身是引用类型;
输入
Java提供的输出包括:System.out.println()
/ print()
/ printf()
,其中printf()
可以格式化输出;
Java提供Scanner对象来方便输入,读取对应的类型可以使用:scanner.nextLine()
/ nextInt()
/ nextDouble()
/ ...
看一个从控制台读取一个字符串和一个整数的例子:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 创建Scanner对象
String name = scanner.nextLine(); // 读取一行输入并获取字符串
int age = scanner.nextInt(); // 读取一行输入并获取整数
System.out.printf("Hi, %s, you are %d\n", name, age); // 格式化输出
}
}
创建Scanner
对象并传入System.in
。System.out
代表标准输出流,而System.in
代表标准输入流。直接使用System.in
读取用户输入虽然是可以的,但需要更复杂的代码,而通过Scanner
就可以简化后续的代码。
有了Scanner
对象后,要读取用户输入的字符串,使用scanner.nextLine()
,要读取用户输入的整数,使用scanner.nextInt()
。Scanner
会自动转换数据类型,因此不必手动转换。
要测试输入,我们不能在线运行它,因为输入必须从命令行读取,因此,需要走编译、执行的流程:
$ javac Main.java
这个程序编译时如果有警告,可以暂时忽略它,在后面学习IO的时候再详细解释。编译成功后,执行:
$ java Main
Input your name: Bob
Input your age: 12
Hi, Bob, you are 12
根据提示分别输入一个字符串和整数后,我们得到了格式化的输出。
do while循环
在Java中,while
循环是先判断循环条件,再执行循环。而另一种do while
循环则是先执行循环,再判断条件,条件满足时继续循环,条件不满足时退出。它的用法是:
do {
执行循环语句
} while (条件表达式);
可见,do while
循环会至少循环一次。
无论是while
循环还是for
循环,在循环过程中,可以使用break
语句跳出当前循环。我们来看一个例子:
break
会跳出当前循环,也就是整个循环都不会执行了。而continue
则是提前结束本次循环,直接继续执行下次循环。
break
语句可以跳出当前循环;
break
语句通常配合if
,在满足条件时提前结束整个循环;
break
语句总是跳出最近的一层循环;
continue
语句可以提前结束本次循环;
continue
语句通常配合if
,在满足条件时提前结束本次循环。
打印数组内容
使用Arrays.toString()
可以快速获取数组内容。
// 遍历数组
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] ns = { 1, 1, 2, 3, 5, 8 };
System.out.println(Arrays.toString(ns));
}
}
数组排序
可以直接使用Java标准库提供的Arrays.sort()
进行排序;
对数组排序会直接修改数组本身。
多维数组
// 二维数组
public class Main {
public static void main(String[] args) {
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
int[] arr0 = ns[0];
System.out.println(arr0.length); // 4
System.out.println(Arrays.deepToString(ns)); // 二维数组打印
}
}
三维数组
三维数组就是二维数组的数组。可以这么定义一个三维数组:
int[][][] ns = {
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
},
{
{10, 11},
{12, 13}
},
{
{14, 15, 16},
{17, 18}
}
};
字符串和编码
String
String
内部是通过一个char[]
数组表示的,因此,按下面的写法也是可以的:
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
字符串比较
想要比较两个字符串的内容是否相同。必须使用equals()
方法而不能用==
。
要忽略大小写比较,使用equalsIgnoreCase()
方法。
去除首尾空白字符
使用trim()
方法可以移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
:
" \tHello\r\n ".trim(); // "Hello"
注意:trim()
并没有改变字符串的内容,而是返回了一个新字符串。
另一个strip()
方法也可以移除字符串首尾空白字符。它和trim()
不同的是,类似中文的空格字符\u3000
也会被移除:
"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
格式化字符串
字符串提供了formatted()
方法和format()
静态方法,可以传入其他参数,替换占位符,然后生成新的字符串:
// String
public class Main {
public static void main(String[] args) {
String s = "Hi %s, your score is %d!";
System.out.println(s.formatted("Alice", 80));
System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5));
}
}
有几个占位符,后面就传入几个参数。参数类型要和占位符一致。我们经常用这个方法来格式化信息。常用的占位符有:
%s
:显示字符串;%d
:显示整数;%x
:显示十六进制整数;%f
:显示浮点数。
占位符还可以带格式,例如%.2f
表示显示两位小数。如果你不确定用啥占位符,那就始终用%s
,因为%s
可以显示任何数据类型。
字符编码
在Java中,char
类型实际上就是两个字节的Unicode
编码。如果我们要手动把字符串转换成其他编码,可以这样做:
byte[] b1 = "Hello".getBytes(); // 按系统默认编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
注意:转换编码后,就不再是char
类型,而是byte
类型表示的数组。
如果要把已知编码的byte[]
转换为String
,可以这样做:
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
始终牢记:Java的String
和char
在内存中总是以Unicode编码表示。
小结
-
Java字符串
String
是不可变对象; -
字符串操作不改变原字符串内容,而是返回新字符串;
-
常用的字符串操作:提取子串、查找、替换、大小写转换等;
-
Java使用Unicode编码表示
String
和char
; -
转换编码就是将
String
和byte[]
转换,需要指定编码; -
转换为
byte[]
时,始终优先考虑UTF-8
编码。
StringBuilder
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
虽然可以使用 + 直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
为了能高效拼接字符串,Java标准库提供了StringBuilder
,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象:
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();
StringBuilder
还可以进行链式操作:
// 链式操作
public class Main {
public static void main(String[] args) {
var sb = new StringBuilder(1024);
sb.append("Mr ")
.append("Bob")
.append("!")
.insert(0, "Hello, ");
System.out.println(sb.toString());
}
}
如果我们查看StringBuilder
的源码,可以发现,进行链式操作的关键是,定义的append()
方法会返回this
,这样,就可以不断调用自身的其他方法。
仿照StringBuilder
,我们也可以设计支持链式操作的类。例如,一个可以不断增加的计数器:
// 链式操作
public class Main {
public static void main(String[] args) {
Adder adder = new Adder();
adder.add(3)
.add(5)
.inc()
.add(10);
System.out.println(adder.value());
}
}
class Adder {
private int sum = 0;
public Adder add(int n) {
sum += n;
return this;
}
public Adder inc() {
sum ++;
return this;
}
public int value() {
return sum;
}
}
注意:对于普通的字符串+
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
的操作。在运行期,StringConcatFactory
会自动把字符串连接操作优化为数组复制或者StringBuilder
操作。
你可能还听说过StringBuffer
,这是Java早期的一个StringBuilder
的线程安全版本,它通过同步来保证多个线程操作StringBuffer
也是安全的,但是同步会带来执行速度的下降。
StringBuilder
和StringBuffer
接口完全相同,现在完全没有必要使用StringBuffer
。
包装类型
Java的数据类型分两种:
-
基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
-
引用类型:所有
class
和interface
类型
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null
:
String s = null;
int n = null; // compile error!
那么,如何把一个基本类型视为对象(引用类型)?
比如,想要把int
基本类型变成一个引用类型,我们可以定义一个Integer
类,它只包含一个实例字段int
,这样,Integer
类就可以视为int
的包装类(Wrapper Class):
public class Integer {
private int value;
public Integer(int value) {
this.value = value;
}
public int intValue() {
return this.value;
}
}
定义好了Integer
类,我们就可以把int
和Integer
互相转换:
Integer n = null;
Integer n2 = new Integer(99);
int n3 = n2.intValue();
实际上,因为包装类型非常有用,Java核心库为每种基本类型都提供了对应的包装类型:
基本类型 | 对应的引用类型 |
---|---|
boolean | java.lang.Boolean |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
char | java.lang.Character |
我们可以直接使用,并不需要自己去定义:
// Integer:
public class Main {
public static void main(String[] args) {
int i = 100;
// 通过new操作符创建Integer实例(不推荐使用,会有编译警告):
Integer n1 = new Integer(i);
// 通过静态方法valueOf(int)创建Integer实例:
Integer n2 = Integer.valueOf(i);
// 通过静态方法valueOf(String)创建Integer实例:
Integer n3 = Integer.valueOf("100");
System.out.println(n3.intValue());
}
}
int
和Integer
可以互相转换:
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();
所以,Java编译器可以帮助我们自动在int
和Integer
之间转型:
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
这种直接把int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法,称为自动拆箱(Auto Unboxing)。
注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
不变类
所有的包装类型都是不变类。我们查看Integer
的源码可知,它的核心代码如下:
public final class Integer {
private final int value;
}
因此,一旦创建了Integer
对象,该对象就是不变的。
对两个Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较:
// == or equals?
public class Main {
public static void main(String[] args) {
Integer x = 127;
Integer y = 127;
Integer m = 99999;
Integer n = 99999;
System.out.println("x == y: " + (x==y)); // true
System.out.println("m == n: " + (m==n)); // false
System.out.println("x.equals(y): " + x.equals(y)); // true
System.out.println("m.equals(n): " + m.equals(n)); // true
}
}
仔细观察结果的童鞋可以发现,==
比较,较小的两个相同的Integer
返回true
,较大的两个相同的Integer
返回false
,这是因为Integer
是不变类,编译器把Integer x = 127;
自动变为Integer x = Integer.valueOf(127);
,为了节省内存,Integer.valueOf()
对于较小的数,始终返回相同的实例,因此,==
比较“恰好”为true
,但我们绝不能因为Java标准库的Integer
内部有缓存优化就用==
比较,必须用equals()
方法比较两个Integer
。
按照语义编程,而不是针对特定的底层实现去“优化”。
因为Integer.valueOf()
可能始终返回同一个Integer
实例,因此,在我们自己创建Integer
的时候,以下两种方法:
- 方法1:
Integer n = new Integer(100);
- 方法2:
Integer n = Integer.valueOf(100);
方法2更好,因为方法1总是创建新的Integer
实例,方法2把内部优化留给Integer
的实现者去做,即使在当前版本没有优化,也有可能在下一个版本进行优化。
我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()
就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
创建新对象时,优先选用静态工厂方法而不是new操作符。
如果我们考察Byte.valueOf()
方法的源码,可以看到,标准库返回的Byte
实例全部是缓存实例,但调用者并不关心静态工厂方法以何种方式创建新实例还是直接返回缓存的实例。
进制转换
Integer
类本身还提供了大量方法,例如,最常用的静态方法parseInt()
可以把字符串解析成一个整数:
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
Integer
还可以把整数格式化为指定进制的字符串:
// Integer:
public class Main {
public static void main(String[] args) {
System.out.println(Integer.toString(100)); // "100",表示为10进制
System.out.println(Integer.toString(100, 36)); // "2s",表示为36进制
System.out.println(Integer.toHexString(100)); // "64",表示为16进制
System.out.println(Integer.toOctalString(100)); // "144",表示为8进制
System.out.println(Integer.toBinaryString(100)); // "1100100",表示为2进制
}
}
注意:上述方法的输出都是String
,在计算机内存中,只用二进制表示,不存在十进制或十六进制的表示方法。int n = 100
在内存中总是以4字节的二进制表示:
┌────────┬────────┬────────┬────────┐
│00000000│00000000│00000000│01100100│
└────────┴────────┴────────┴────────┘
我们经常使用的System.out.println(n);
是依靠核心库自动把整数格式化为10进制输出并显示在屏幕上,使用Integer.toHexString(n)
则通过核心库自动把整数格式化为16进制。
这里我们注意到程序设计的一个重要原则:数据的存储和显示要分离。
Java的包装类型还定义了一些有用的静态变量
// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;
// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
// long类型占用的bit和byte数量:
int sizeOfLong = Long.SIZE; // 64 (bits)
int bytesOfLong = Long.BYTES; // 8 (bytes)
最后,所有的整数和浮点数的包装类型都继承自Number
,因此,可以非常方便地直接通过包装类型获取各种基本类型:
// 向上转型为Number:
Number num = new Integer(999);
// 获取byte, int, long, float, double:
byte b = num.byteValue();
int n = num.intValue();
long ln = num.longValue();
float f = num.floatValue();
double d = num.doubleValue();
处理无符号整型
在Java中,并没有无符号整型(Unsigned)的基本数据类型。byte
、short
、int
和long
都是带符号整型,最高位是符号位。而C语言则提供了CPU支持的全部数据类型,包括无符号整型。无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成。
例如,byte是有符号整型,范围是-128
~+127
,但如果把byte
看作无符号整型,它的范围就是0
~255
。我们把一个负的byte
按无符号整型转换为int
:
// Byte
public class Main {
public static void main(String[] args) {
byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
}
}
因为byte
的-1
的二进制表示是11111111
,以无符号整型转换后的int
就是255
。
类似的,可以把一个short
按unsigned转换为int
,把一个int
按unsigned转换为long
。
如果我们要生成一个区间在[MIN, MAX)
的随机数,可以借助Math.random()
实现,计算如下:
// 区间在[MIN, MAX)的随机数
public class Main {
public static void main(String[] args) {
double x = Math.random(); // x的范围是[0,1)
double min = 10;
double max = 50;
double y = x * (max - min) + min; // y的范围是[10,50)
long n = (long) y; // n的范围是[10,50)的整数
System.out.println(y);
System.out.println(n);
}
}
Random
Random
用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。
要生成一个随机数,可以使用nextInt()
、nextLong()
、nextFloat()
、nextDouble()
:
Random r = new Random();
r.nextInt(); // 2071575453,每次都不一样
r.nextInt(10); // 5,生成一个[0,10)之间的int
r.nextLong(); // 8811649292570369305,每次都不一样
r.nextFloat(); // 0.54335...生成一个[0,1)之间的float
r.nextDouble(); // 0.3716...生成一个[0,1)之间的double
有童鞋问,每次运行程序,生成的随机数都是不同的,没看出伪随机数的特性来。
这是因为我们创建Random
实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。
如果我们在创建Random
实例时指定一个种子,就会得到完全确定的随机数序列
import java.util.Random;
public class Main {
public static void main(String[] args) {
Random r = new Random(12345);
for (int i = 0; i < 10; i++) {
System.out.println(r.nextInt(100));
}
// 51, 80, 41, 28, 55...
}
}