Java
约 3405 个字 219 行代码 预计阅读时间 14 分钟
这里是站在已经掌握了 C++ 的基础上来学习 Java 的,所以一些完全相同的东西就不再赘述了。
简介
Java 最早是针对小型家电设备的嵌入式应用开发而设计的语言,最开始反响平平,但是后来互联网兴起后,Java成为了互联网开发的主流语言。Java 介于解释型和编译型语言之间,先将代码编译成一种“字节码”,再由JVM解释执行。
随着Java的发展,Java又分出了三个不同版本的规范:
- Java SE:Standard Edition
- Java EE:Enterprise Edition
- Java ME:Micro Edition
它们之间的包含关系是\(\text{ME} \subset \text{EE} \subset \text{SE}\),但是Java ME从未真正流行过,Java EE 也已经停止更新。
Java 中常见的缩写解释:
- JVM:Java 虚拟机,相当于一个解释器
- JDK:Java 开发工具包,JRE + 开发工具
- JRE:Java 运行环境,JVM + Java 标准库
基础
public class HelloWorld {
public static void main(String[] args) {
// 输出 Hello World
System.out.println("Hello World");
}
}
Java 中所有方法和变量都必须定义在类中,JVM默认main函数是程序的入口,并且这个函数应该是static的,如果没有运行时会报错。
Java 中的注释有三种:
//:单行注释/* */:多行注释/** */:文档注释
Java 代码编译的过程:Java 代码文件的后缀名是.java,被Java编译器编译之后编译成.class文件,之后在JVM中运行。
基础数据类型
Java 中的类型可以分为值类型和引用类型,基础值类型和 C++ 差不多,有:
- 整数类型:
byte,short,int,long - 浮点数类型:
float,double - 字符类型:
char - 布尔类型:
boolean
一些说明:
- Java只定义了带符号的整型
- Java对布尔类型的存储并没有做规定,通常JVM内部会把
boolean表示为4字节整数 - Java的
char类型除了可表示标准的ASCII外,还可以表示一个Unicode字符,无论中英文都是占两个字节
除了上述基本类型的变量,剩下的都是引用类型,在Java中,引用类型的变量非常类似于C/C++的指针,引用类型指向一个对象,所有引用类型的默认值都是null。对于基本类型,Java提供了对应的包装类型,比如int对应的包装类型是Integer,Integer就是一个引用类型,这个主要是为了方便引用传参。
常量使用final关键字定义,也就是C++里面的const,除此之外final还可以用于限制方法和类,限制方法后,该方法不能被重写,限制类后,该类不能被继承。此外,Java 支持使用var来进行自动类型推断,用法同C++的auto。
Java中的类型转换还是和C++差不多,对于short + int这种会自动向上提升为int,即隐式类型转换,显式类型转换还是类似于(int) a。
字符串
String是引用类型,Java中的String也是支持+的(这个只能说是特殊考虑,Java其实是不支持运算符重载的),对于非字符串类型,Java会自动尝试类型转换。
对于格式化字符串,Java支持使用String.format()来格式化字符串。
Java 支持多行字符串,和Python一样是三引号""",此外Java中的字符串同样是不可变的。最后,这里给出一些常用的方法,基本是如今高级语言都会有的,对于单个字符:
isDigit(ch) // 判断是否为数字
isLetter(ch) // 判断是否为字母
isLetterOrDigit(ch) // 判断是否为字母或数字
toLowerCase(ch) // 转换为小写
toUpperCase(ch) // 转换为大写
isLowerCase(ch) // 判断是否为小写
isUpperCase(ch) // 判断是否为大写
对于字符串:
length() // 获取长度
isEmpty() // 判断是否为空
equals(str) // 判断是否相等
isBlank() // 判断是否为空白
trim() // 去除首尾空格
toUpperCase() // 转换为大写
toLowerCase() // 转换为小写
compareTo(str) // 比较字符串
startsWith(str) // 判断是否以str开头
endsWith(str) // 判断是否以str结尾
数组
Java 中的数组是引用类型,定义方式如下,中括号也可以放在后面(但是不推荐)
可以使用length来获取长度,但这个length是数组的属性,而不是方法。
数组中的元素如果未进行赋值会被自动设为默认值,Java中的[]会进行越界检查,如果越界会抛出ArrayIndexOutOfBoundsException异常。
Java的标准库已经内置了排序功能,我们只需要调用JDK提供的Arrays.sort()就可以排序。
如果需要自定义排序规则,可以传入一个Comparator。
lambda 表达式
上面的Arrays.sort(arr, (a, b) -> a > b ? 1 : -1);就是使用了 lambda 表达式,lambda 表达式是 Java 8 引入的,它提供了一种简洁的方式来表示匿名函数,本质是函数式接口的一个匿名实现。函数式接口是指只包含一个抽象方法的接口,Java 8 在 java.util.function 包中预定义了许多常用的函数式接口,例如:
Consumer<T>:接收一个参数,没有返回值。Predicate<T>:接收一个参数,返回一个 boolean 类型的值。Function<T, R>:接收一个 T 类型的参数,返回一个 R 类型的值。Supplier<T>:不接收参数,返回一个 T 类型的值。
Lambda 表达式的基本语法如下:
Lambda 表达式还有一种更简洁的形式,叫做方法引用(Method Reference),类似于函数指针。当 Lambda 表达式只是简单地调用一个已存在的方法时,可以使用方法引用来代替。
// Lambda 表达式
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
names.forEach(name -> System.out.println(name));
// 方法引用
names.forEach(System.out::println);
分支与循环
这个真和C++没有什么不同,都是if-else,switch,while,do-while,for和for-each,并且有break和continue。
Java 中的for-each的遍历,格式一般为:
比如对于数组
输入输出
流
- 字节流是所有 I/O 的基础,其抽象基类分别是
InputStream和OutputStream。 - 字符流是字节流的包装,其抽象基类分别是 Reader 和 Writer。它们通常用于处理文本文件。
- FileReader:从文件中读取字符。
- FileWriter:向文件中写入字符。
try (
FileReader fr = new FileReader("source.txt");
FileWriter fw = new FileWriter("destination.txt")
) {
int charRead;
// 每次读取一个字符,直到文件末尾
while ((charRead = fr.read()) != -1) {
fw.write(charRead);
}
System.out.println("文件复制成功!");
} catch (IOException e) {
e.printStackTrace();
}
- 缓冲流是对基本 I/O 流的装饰,它们在内存中设置一个缓冲区,批量读写数据,从而大大提高效率。
标准输入输出
Java 的标准输入输出(键盘输入和屏幕输出)也通过流来实现:
- 标准输入流:
System.in(类型为InputStream)。 - 标准输出流:
System.out(类型为PrintStream)。 - 标准错误流:
System.err(类型为PrintStream)。
在实际开发中,我们通常会使用 Scanner 类或 BufferedReader 来方便地处理键盘输入。
使用 Scanner 读取键盘输入:
Scanner scanner = new Scanner(System.in);
System.out.print("请输入你的名字:");
String name = scanner.nextLine();
System.out.print("请输入你的年龄:");
int age = scanner.nextInt();
System.out.println("你好," + name + "!你今年 " + age + " 岁。");
scanner.close();
Package
package 可以方便管理和组织java文件的目录结构,防止不同文件之间的命名冲突,类似于命名空间,package 作为Java代码源文件的第一条语句,如果缺省则指定为无名包。package是一个多层的树形结构,比如JDK的Arrays类存放在包java.util下面,因此,完整类名是java.util.Arrays,编译后的.class文件和源文件的目录结构保持一致,在JVM中运行时只看完整的类名。
使用import语句可以导入其他包中的类,import语句可以放在文件的任何位置,但是通常放在文件的开头。
如果需要导入包中的所有类,可以使用*,但是不推荐。
集合类
Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口,这里主要讨论最常用的三个具体的集合类,除此以外,基础数据结构中的Stack,Queue,PriorityQueue也都是有的。
ArrayList
List 代表一个可变长的数组,即C++中的vector,本身是一个接口,Java中提供了ArrayList和LinkedList两种实现,前者基于数组实现,后者基于链表实现,这个接口包含了以下方法:
- 在末尾添加一个元素:
add - 在指定索引添加一个元素:
add(int index, Type val) - 删除某个元素:
remove(Type val) - 删除指定索引的元素:
remove(int index) - 获取指定索引的元素:
get(int index) - 获取大小(包含元素的个数):
size() - 判断是否包含某个元素:
contains(Type val) - 判断是否为空:
isEmpty() - 清空:
clear()
创建List,这里使用ArrayList
Java 中也是有迭代器的,迭代器的遍历效率往往最高
HashMap
HashMap用于存储键值对,它主要提供以下几个方法:
- 将键值对添加进HashMap:
put(Type key, Type value) - 将键值对从HashMap删除:
remove(Type key) - 获取键对应的值:
get(Type key) - 判断是否包含某个键:
containsKey(Type key) - 判断是否为空:
isEmpty() - 清空:
clear()
遍历可以是遍历键,也可以是遍历值,还可以是遍历键值对。
var map = new HashMap<Integer, String>();
map.put(1, "a");
map.put(2, "b");
map.put(3, "c");
for(var key : map.keySet()) {
System.out.println(key);
}
for(var value : map.values()) {
System.out.println(value);
}
for(var entry : map.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
HashSet
HashSet用于存储不重复的元素集合,它主要提供以下几个方法:
- 将元素添加进Set:
add(Type value) - 将元素从Set删除:
remove(Type value) - 判断是否包含元素:
contains(Type value) - 判断是否为空:
isEmpty() - 清空:
clear()
HashSet 相当于封装了 HashMap,值全为null,莫名感觉和GO殊途同归。
异常
Java 中的异常处理和 C++ 差不多,使用经典的try-catch来捕获异常,使用throw来抛出异常,使用finally来执行一些必须执行的代码。
Java 中的异常类都是继承自Exception,自定义异常时一般是选择一个异常作为根异常,然后在此基础上继承,通常会选择RuntimeException作为根异常。
public class MyException extends RuntimeException {
public MyException(String message) {
super(message);
}
}
Java 的断言也是异常,使用assert关键字,断言失败会抛出AssertionError异常。
int a,b;
System.in.readInteger(a);
System.in.readInteger(b);
assert b != 0 : "b cannot be 0";
System.out.println(a/b);
JVM默认关闭断言指令,即遇到assert语句就自动忽略了,不执行。要执行assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)参数启用断言。
面向对象
方法
Java 中访问修饰符有public,private和protected以及static,然后参数只支持按位置传递,有变长参数,也有this变量指向当前对象,方法重载也是支持的。
public class Test {
// 经典的重载
public void test(int a, int b) {
System.out.println(a + b);
}
public void test(double a, double b) {
System.out.println(a + b);
}
// 可变长参数
public void test(String... args) {
for (String arg : args) {
System.out.println(arg);
}
}
}
构造函数依旧是如果没有编译器会给一个默认的,并且可以给字段默认值,如果没有显式写出,那么默认值就是对应类型的默认值,比如int的默认值是0。
public class Test {
int a = 1;
int b = 2;
public Test() {
}
public Test(int a, int b) {
this.a = a;
this.b = b;
}
}
继承
Java 所有类最终都继承自Object,Java中只能单继承,继承使用extends关键字,Java 中有super关键字表示父类,子类引用父类的字段时,可以用super.fieldName,但是没有必要,super主要用于调用父类的构造函数super()。
还是一样,向上转型是安全的,向下转型则不一定,Java 中有instanceof关键字来判断类型。
多态
Java 重写父类方法时,需要使用@Override注解,重写方法时,参数列表必须和父类方法一致,返回类型必须是父类方法返回类型的子类型,或者void。Java 支持全面的动态绑定,也就是不需要声明虚函数。
Java 提供abstract关键字来定义抽象类和抽象方法,抽象类不能被实例化,抽象方法没有方法体,只有方法签名。
但是对于一个全是抽象方法的类,更推荐写为Interface,接口只规约需要实现的方法,不允许有字段,并且默认方法是public abstract的,实现接口使用implements关键字,Java 中允许实现多个接口。
泛型
通常来说,泛型类一般用在集合类中,例如ArrayList<T>,我们很少需要编写泛型类。
// 泛型类 Box,可以存放任何类型的对象
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
在没有泛型之前,对于这种List,可能需要创建一个可以存放 Object 的类,然后在取出时进行强制类型转换,这会带来运行时 ClassCastException 的风险。
泛型接口的定义方式与泛型类类似。
public interface MyList<E> {
void add(E e);
E get(int index);
}
public class MyArrayList<E> implements MyList<E> {
// ... 实现接口方法
}
泛型方法可以在任何类中定义,无论该类是否是泛型类。类型参数<T>放在方法的返回类型之前。
// 泛型方法,可以打印任何类型的数组
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.printf("%s ", element);
}
System.out.println();
}
有时可能需要处理未知类型的泛型,这时就需要使用通配符?。上界通配符:<? extends T>表示类型是 T 或 T 的子类。这通常用于读取数据的场景。
public void printList(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
下界通配符:<? super T>表示类型是 T 或 T 的父类。这通常用于写入数据的场景。
public void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
反射
反射指的是在运行期间可以拿到一个对象的全部信息,然后可以动态地访问和修改对象的属性和方法。对于一个Java类,JVM在加载类时会创建一个Class对象(注意首字母是大写的),这个对象包含了类的全部信息,包括字段、方法、构造函数等。
要使用反射,首先要获取类的 Class 对象,有三种主要方法:
Class.forName():最常用,通过类的全限定名(包名 + 类名)获取。
Object.getClass():通过对象获取 Class 对象。
Class.class:通过类字面量获取 Class 对象。
下面是一个简单的例子,演示如何通过反射动态调用一个方法。
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void sayHello(String greeting) {
System.out.println(greeting + ", my name is " + name);
}
}
import java.lang.reflect.Method;
public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 1. 获取 Class 对象
Class<?> personClass = Class.forName("Person");
// 2. 获取构造方法并创建对象
// 这里假设 Person 类只有一个带 String 参数的构造方法
java.lang.reflect.Constructor<?> constructor = personClass.getConstructor(String.class);
Object person = constructor.newInstance("Tom");
// 3. 获取 sayHello 方法
Method sayHelloMethod = personClass.getMethod("sayHello", String.class);
// 4. 调用方法
sayHelloMethod.invoke(person, "Hello"); // 输出: Hello, my name is Tom
}
}
注解
Java 注解(Annotation)是一种特殊的元数据,可以为代码提供信息,但它本身不会对代码的执行产生直接影响。你可以把注解想象成贴在代码上的标签,这些标签可以被编译器、JVM 或其他工具读取和处理。
Java 注解主要用于以下几个方面:
- 编译时检查
- 生成代码或配置文件
- 运行时处理
Java 内置了许多标准注解,同时你也可以自定义注解,以下是一些最常见的内置注解:
@Override:标记一个方法是重写父类的方法。如果父类中没有这个方法,编译器会报错。@Deprecated:标记一个类、方法或字段已经过时,不建议再使用。当你在代码中使用它时,编译器会发出警告。@SuppressWarnings:告诉编译器忽略特定的警告。