由于各种原因,我在平时写代码或刷题时,每隔一定时期的就要进行语言切换(C、C++、Python、JavaScript、Java、Go),导致一些常用的语法或写题技巧老是记混,每次卡壳都要去搜一搜。

本文将随着刷题进度持续记录一些使用 Java 语言刷题时必须记住的要素,快速解决在语法方面的障碍。如果你打算开始使用 Java 语言进行刷题时,可以通篇浏览实现快速热身。

本文主要内容:

  • 必背写法与重要函数用法
  • 集合增删改查与字符串操作速查
  • 语言无关的通用技巧

ACM 模式下的输入输出详看:站内文章【语言热身】Java 编写其他输入输出模式

本文题目难度标识:🟩简单,🟨中等,🟥困难。

Java 语法基础

代码风格

看看爪哇代码长啥样先,试图唤起尘封的记忆 😅😅😅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Solution{                             // 惯例一个文件一个类,文件名与类名相同
public static void main(String[] args){ // main 函数的写法
// 变量的声明方式
int n=1,m; // 声明变量+初始化
final int MAX_WIDTH = 800; // 常量值
int MOD = 1_000_000_007; // 数字表示
Object obj = null; // 空值
Map<Integer,String> map = new HashMap<>(); // 声明对象

// 迭代方式
for(int i=0;i<3;i++){}; // 基础for循环
for(int e:nums){}; // foreach 语法格式
}
}

讨论类的定义和静态上下文的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一个文件可以有多个类,但只能有一个与文件名同名的public类
public class Solution{

class ClassA{} // 非静态内部类
static class ClassB{}
public static void main(String[] args){
// 如果在静态方法中直接引用非静态成员,Java 编译器无法确定应该访问哪个实例的成员,因此会报错。
// ClassA a = new ClassA(); // cannot be referenced from a static context
ClassB b = new ClassB();
ClassC c = new ClassC();
}

public void func(){
ClassA a = new ClassA();
}

}

class ClassC{} // 包级私有类

操作符

一些注意点:

操作 说明 备注
右移操作 算数右移:>>
逻辑右移:>>>
记得和别的语言进行区分
取模运算 由于 Java 取模的特殊性,当被除数为负数时取模结果为负数,需要纠正:

int modulus = (sum % k + k) % k;
相关题目:🟨 974. 和可被 K 整除的子数组 - 力扣(LeetCode)
优先级 恒等运算符优先级比位运算符更大,因此正确写法为:
(num&mask)==0x01
这是语言之间普遍的优先级

值传递

有时候搞不清 Java 到底是引用传递还是值传递,根本原因是「值传递」没有理解透。

Java 中的函数传递本质是值传递的。

函数中如果传递的参数是对象 Object,那么传递的只是对象的引用。我们可以利用这个引用去修改对象,但如果在函数内重新 new Object(),函数返回时,主函数不会发生变化。

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
String str = "Hello";
modifyString(str);
System.out.println(str); // 输出:Hello
}

public static void modifyString(String str) {
str = "World"; // str现在指向了一个新的字符串对象 "World"
System.out.println(str); // 输出:World
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Node {
int value;
Node(int value) {
this.value = value;
}
}

public class Test {
public static void main(String[] args) {
Node node = new Node(10);
modifyNode(node);
System.out.println(node.value); // 输出:20
}

public static void modifyNode(Node node) {
node.value = 20; // 修改 node 对象的 value 属性
}
}

此外需要注意的是 Java 在传递参数给方法前,都会计算参数的值。后文聊到 MapgetOrDefault 方法时会提到。

数组 Arrays

声明与初始化

1
2
3
4
5
6
7
8
9
10
11
12
// 一维数组
int[] nums1 = new int[n]; // 动态初始化 初始值 0、null(对于对象数组)
int[] nums2 = new int[]{1,2,3,4,5}; // 静态初始化
int[] nums3 = {5,4,3,2,1}; // 省掉 new int[]
int[] nums4 = new int[]{v,printInfo()}; // 静态初始化接受变量和返回值


// 二维数组
int[][] a = {{1,2,3},{4,5,6},{7,8,9,10}}; // 省掉 new int[][]
int[][] b = {{},{2},{},{3,5}}; // 省掉 new int[][]
int [][]m = new int[3][]; // 每列指向一个 null 值
int [][]n = new int[3][2]; // 每格初始化为 0

排序 Arrays.sort

详看:站内文章【语言热身】Java 中的排序

Arrays.stream

Arrays.stream() 可以将数组转换为流,以使用更多的操作。

1
2
3
4
5
6
int[] arr1 = {1, 2, 3, 4, 5};  
int sum = Arrays.stream(arr1).sum();
int max = Arrays.stream(arr1).max() // OptionalInt
.getAsInt(); // int
int min = Arrays.stream(arr1).min().getAsInt();
double avg = Arrays.stream(arr1).average().getAsDouble();

其他

使用 Arrays.equals(array1, array2); 比较两个数组的元素是否相等。

1
2
3
4
5
// 比较两个数组是否相等
int[] array1 = new int[]{1,2,3};
int[] array2 = new int[]{1,2,3};
int[] array3 = array1;
Arrays.equals(array1, array2); // 判断两个数组是否相等。以上3个数组使用Arrays.equals比较均为true

Arrays.copyOf() 方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。Arrays.copyOf() 的第二个参数指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。

1
2
3
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = Arrays.copyOf(arr1, 3); // arr2 = [1,2,3]
int[] arr3 = Arrays.copyOf(arr1, 10); // arr3 = [1,2,3,4,5,0,0,0,0,0]

更多关于 Arrays.copyOf() 的源码解读详见文章:站内文章搞清楚 Java 中 List 的 toArray 方法

Arrays.fill() 将指定的 int 值分配给指定 int 型数组的每个元素。

1
2
int[] arr1 = {1, 2, 3, 4, 5};  
Arrays.fill(arr1, 9); // arr = [9,9,9,9,9]

字符串 String

1
2
3
4
5
// 字符串相等判断
String str1 = "hello"; // 定义字符串常量
String str2 = new String("hello");
boolean isEquals1 = str1.equals(str2); // 字符串的比较 true
boolean isEquals2 = str1 == str2 // false
操作 String StringBuffer/StringBuilder
+ 操作符。注意从左到右结合性 sb.append(str);
sb.insert(int offset, str);
不可变类 sb.delete(start, end)
sb.deleteCharAt()
改(替换字符) replace(char oldChar, char newChar):返回新的字符串,原字符串不改变。
replaceAll(String regex, String replacement) :返回新的字符串,原字符串不改变。
replaceFirst(String regex, String replacement):返回新的字符串,原字符串不改变。
sb.replace(start,end, str);:将起始位置为 start,结束位置为 end-1 的子串替换为 str。不生成新的 StringBuilder 对象,在原来的 StringBuilder 对象上修改

sb.setCharAt(index,c)
str.charAt(index); 相同
长度 str.length();

StringBuilder 还提供以下函数:

  • sb.reverse():反转字符串
  • sb.toString():返回字符串
  • sb.capacity():返回当前容量。注意,和返回有效长度 sb.length() 不一样!

String 子串的获取:

  • str.substring(int beginIndex)
  • str.substring(int beginIndex,int endIndex)。不含 endIndex

String 大小写转换:

  • str.toUpperCase()
  • str.toLowerCase()

String 查字符、字符串第一次出现的位置:

  • str.indexOf(c)

字符、字符串与数字之间的转换

单步骤转换:

image.png

一招静态方法 valueOf 可以解决大部分转换难题。被转的放里面。

Character[] char[] 互转详见:站内文章【语言热身】Java 中的排序

我们在处理输入输出字符串题目时的连招:

1
2
3
4
5
6
7
8
// 把字符串转换成字符数组处理,更丝滑。不然只能疯狂 s.charAt(i) 咯
public String mySolution(String s){
char[] cc = s.toCharArray(); // 将题目输入字符串 `s` 转换为临时数组 `cc`
/*
对数组 `cc` 进行处理
*/
return new String(cc);
}

StringBufferStringBuilder 中的构造方法和其他方法几乎是完全一样的。StringBuilder 非线程安全,但速度更快。一般情况下(比如刷题)推荐使用 StringBuilder

String 保留小数精度的处理可看 站内文章【语言热身】Java 编写其他输入输出模式

基础函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 输出
System.out.println(); // 输出并换行
System.out.print(); // 输出不换行

// 数学与常量
Math.max(num1,num2);
Math.min(num1,num2);
Math.abs(num);
Math.pow(base, exp);
Math.log(num); // 以 e 为底。对于非常规底数可使用换底公式 log_x(y)=ln(y)/ln(x);
Integer.MAX_VALUE;
Integer.MIN_VALUE;

// 随机数
Random rand = new Random(); // java.util.Random
rand.nextInt(num); // int [0,num)
Math.random(); // double [0.0,1.0)

// 【知识点】长度的获取
int len1 = nums.length; // 数组长度
int len2 = str.length(); // 字符串长度
int len3 = map.size(); // 集合类长度

集合 Collection/Map

image.png

详看:站内文章【语言热身】Java 集合的使用

调试(标准化输出)

1
2
System.out.println(object); // 标准输出
// 原理:String.valueOf(object) -> object.toString()

ObjecttoString() 默认实现并不直观。

对数组进行「友好」的字符串转换:

1
Arrays.toString(arr);// 将数组转换为可视字符串

ArrayListHashSet 继承了 AbstractCollection,里面重写了 toString 。我们可以这样直观查看 ArrayList 中的内容:

1
2
3
4
5
System.out.println(c);
// [1,2,3,4,5]

// 或者炫技:
c.forEach(System.out::println);

AbstractCollectiontoString 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";

StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
// sb.append 调用了 String.valueOf()
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}

多线程编程

本章节主要就是背单词!

LeetCode 中的多线程编程题目不多,掌握以下同步模式可解决大部分的问题:

Semaphore 信号量

1
2
3
Semaphore s = new Semaphore(0);
s.acquire(); // s.acquire(2);
s.release(); // s.release(2);

语言无关的通用技巧

数学常识详看:站内文章【刷题日记】算法题常用数学算法和技巧

1
2
3
4
5
6
7
8
9
10
// double计算时提高精度
double y;
int iy;
iy = (int)((y) + 0.5) / 1.0) // 先四舍五入到合适精度
if(Math.abs(iy-y)<0.0001)y=iy; // 视情况设范围

// 利用类型转换将字符类型对应到相应的数组位置。适用于字符串内容均为1种字符的情况。
arr1[str.charAt(i)-'a']++; // a 放到数组下标 0,z放到数组下标 25
arr2[str.charAt(i)-'0']++;
arr3[str.charAt(i)-'A']++;

有时候有些对输入顺序有要求的,我们可以稍微调整一下顺序。

1
2
3
4
5
6
7
8
9
10
11
// 函数参数交换
public static Object foo(int param1, int param2) {
if (...){
// 在指定条件下调换顺序,省去手动转移的麻烦
return foo(param2, param1);
}
/*
按既定参数顺序的函数逻辑
*/
return result;
}

魔数

写题时常见的初始化最大最小值:

  • 最大值:0x3f3f3f3f
  • 最小值:0xc0c0c0c0

根据不同题目要求,我们还可以将最大值设置为 Integer.MAX_VALUE/2(防止相加溢出)

为什么 ACM 中经常使用 0x3f3f3f3f 作为整型的最大值?

原因如下:

  1. 0x3f3f3f3f 的十进制是 1,061,109,567,是 10^9 级别的,而一般场合下的数据都是小于 10^9 的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形。而平时用的 Integer.MAX_VALUE=0x7fffffff 不能满足“无穷大加一个有穷的数依然是无穷大”这个条件,加上一个数后,根据补码规范它会变成了一个很小的负数。
  2. 0x3f3f3f3f 的 2 倍还是一个「无穷大」的数。
  3. 0x3f3f3f3f 的每 8 位(每个字节)都是相同的。我们在程序设计中经常需要使用 memset(a, val, sizeof(a)) 初始化一个数组 a,该语句把数值 val0x00~0xFF)填充到数组 a 的每个字节上,所以用 memset 只能赋值出“每 8 位都相同”的 int。当需要把一个数组中的数值初始化成正无穷时,为了避免加法算术上溢出或者繁琐的判断,我们经常用 memset(a, 0x3f, sizeof(a)) 给数组赋 0x3f3f3f3f 的值来代替。

10910^9 次方级别的大质数:1000000007。在一些题目中会很有用:🟥 Problem - 1632D - Codeforces。对于多数题目来说,这个数常用于将结果取余。

模板写法

单调栈/队列

原型题: 🟩 496. 下一个更大元素 I - 力扣(LeetCode)

1
2
3
4
5
6
// 假设数组A从左到右遍历
insert x // x 为数组中某一个元素
// 假设维护的是一个单调递增的栈(自栈顶向下单调递增)
while !stack.empty() && stack.top()<x
y = stack.pop() // 这也意味着y右侧第一个比y大的元素是x
stack.push(x)

有时候,单调栈或单调队列中不一定存的是实际数字的值,有可能存的是指向实际数字的指针或下标。比如这一题:🟥 239. 滑动窗口最大值 - 力扣(LeetCode)

滑动窗口

样例题:🟨 3. 无重复字符的最长子串 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int lengthOfLongestSubstring(String s) {
Set<Character> set = new HashSet<>(); // 滑动窗口中的内容

int res = 0;
int len = s.length();
int l = 0; // 左指针
for(int i=0;i<len;i++){ // 右指针
while(set.contains(s.charAt(i))){
set.remove(s.charAt(l)); // 弹出滑动窗口内容
l++;
}
set.add(s.charAt(i));
res= Math.max(set.size(),res); // 更新答案
}
return res;
}

方向移动

如果需要物体在规定的棋盘下(比如 n*n)进行方向移动,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义方向
static int[][] directions = new int[][]{
{-1,0},{1,0},{0,-1},{0,1} // 上下左右
};

// 遍历方向
// 传入当前位置i,j,以及棋盘
public void recursion(int i,int j, int[] board){
/* 省略此处递归边界判断 */

for(int k=0;k<directions.length;k++){
int newI = i + directions[k][0];
int newJ = j + directions[k][1];
if(newI>=0&&newI<n&&newJ>=0&&newJ<=n){ // 边界判断
recursion(newI,newJ,board); // 走下一步
}
}
}

相关题目:🟨 688. 骑士在棋盘上的概率 - 力扣(LeetCode)

本文参考