Java 概要
Java 是一个严格的 OO 语言,而且 Java 为不同机器上代码的执行提供了一致性,Java 大部分概念和 C 相似,因此这里只着重解释 Java 与 C 的相异部分
数据类型
Java 中分为基本数据类型、复合数据类型和类
基本数据类型和 C++ 基本相同,占据空间相同,为 byter, char, int, long, float, double, boolean
复合数据类型为数组
类
基本数据类型使用值语义,被构建在栈上。复合数据类型和类被构建在堆上,程序员持有的是对对象的 引用
Java 中不存在窄化隐式类型转换
Java 中的所有变量都有默认值
普通的数据类型(float, double)在高精度场景下是完全不能用的,例如表示货币。这种情况下应当使用 BigInteger 或 BigDecimal
除此之外,还有基础类型对应的 包装类型 ,包装类型和基础类型之间可以相互转换
包装类型和基本类型不同之处在于
包装类型是类
包装类型是不可变的
包装类型提供了大量的成员方法
由于类存在 null,因此在包装类型向基础类型转换时可能报空指针异常
Lambda
Java 中的 Lambda 无法用于元素为基础类型的对象。一个简单的使用是:
Integer[] arr = {1, 2, 3, 4, 5};
Arrays.sort(arr, (a, b) -> {
return b - a;
});
实际上 Lambda 是受注解 FunctionalInterface 约束的单方法接口。
此外,Java 中也有函数指针的概念:
static int reduce(int a, int b) {
return b - a;
}
public static void main(String[] args) throws Throwable {
Integer[] arr = {1, 2, 3, 4, 5};
Arrays.sort(arr, Study::reduce);
System.out.println(Arrays.toString(arr));
}
另外,Java 中因为没有指针的存在,因此无法实现 C++ 中那样简洁的 swap 函数,一般实现方式为:
static void swap(int[] data, int a, int b) {
int tmp = data[a];
data[a] = data[b];
data[b] = data[a];
}
面向对象
Java 中的权限修饰符分为四种:default, private, protect, public
权限 | 解释 |
default | 仅同包可见 |
private | 仅自己可见 |
protect | 同包或之类可见 |
public | 均可见 |
与 C 中不同的是,Java 中的权限修饰符是单独作用于变量或函数的,而不是类似 C 中权限域的概念
还有一些其它区别:
Java 中的类的末尾无需添加分号
Java 中每个文件只能存在一个 public 类
Java 无需前置类型声明
Java 没有 由于单参数构造函数 的隐式类型转换
Java 中亦存在原地初始化,同时构造函数内也可以进行初始化,类中的一个字段初始化顺序为:
执行字段的原地初始化
进入构造函数后再执行一次初始化
Java 中也有抽象类,具体使用方式是使用 abstract 声明一个抽象方法。或者是直接声明一个抽象类
含有抽象方法的类不能被实例化
使用 abstract class 定义的类中只能包含抽象方法
Java 使用 getter 和 setter 读写数据,这种命名规范成为 Java Bean
Java 中的初始化块
Java 初始化会包含三部分代码:
public class Study {
static {
// 静态初始化块
}
{
// 构造代码块
}
public Study(){
// 构造函数
}
}
三种初始化块的执行顺序依次是 静态初始化块 → 构造代码块 → 构造函数
静态初始化块只会在类的第一个对象构造时执行一次。毕竟是静态块 |
继承
Java 继承的特点可以简单总结为:
Java 中所有类都会隐式继承一个父类 Object,利用此特点可以实现异质容器。
Java 中只有使用 extends 一种继承方式,这种方式类似于 C++ 中的 public 继承
Java 中只有单继承
Java 中向下转型必须使用强制类型转换
Java 中可以使用 super 代之父类
Java 中子类的构造函数第一行必须添加父类的构造函数,否则由编译器添加一行父类的默认构造
Java 中子类的字段名不得与父类相同
一般而言,C++ 中的多继承一般是接口继承,Java 亦有接口继承,但是其与普通继承完全分离,使用的关键字是 implements 而不是 extends:
interface Person {
void run();
String getName();
}
class Student implements Person, Hello { // 实现了两个interface
}
接口用于代替接口继承,一个类可以实现多个接口
|
包
Java 中类的完整名字为 包名.类名。包类似于 C++ 中的名字空间,但是一个文件只能属于一个包,而且应当在 java 文件的头部声明。
包之间没有父子关系,java.util 和 java.util.zip 是不同的包 |
Java 文件包组织的方式应当与路径组织的方式相同
默认情况下编译器会为我们导入当前包中其它的 class 和 java.lang.*
Java 还可以将代码打包成 jar 文件,要创建 jar 文件只需要简单地将源代码创建为 zip,然后更改后缀名即可
Object
Object 有几个重要的函数:
Object.hashCode方法按算法将 将该对象的内部地址 映射为一个数值,其值为:
若两个对象相等,则他们的哈希码应该相等。
若两个对象不相等,不要求他们的哈希码不等。
因此,使用哈希码不能用来判断对象是否相等。
Object.clone 用于实现深拷贝,具体方式为:
实现 Cloneable 接口
调用 super.clone() 对对象进行深拷贝
对类的数据成员进行深拷贝
class Child extends Parent implements Cloneable {
public Object clone() {
Parent copy = null;
try {
copy = (Child) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
copy.sumArray = this.sumArray.clone();
return copy;
}
}
字符串
Java 中的字符串归属于一个 String 类,字符串底层使用 char[] 储存数据,因此字符串是不可变的。要比较字符串是否相同,应当使用字符串的 equals 方法而不是 == ,因为 == 比较的实际上是两个引用。
Java 中的 char 类型是 Unicode,在使用 getBytes 进行编码后得到的类型为 byte
在最新版本的 JDK 中,String 内部使用 byte[] 而不是 char[] 进行了优化 |
字符串允许使用
进行字符串连接,但是效率不高(毕竟字符串是不可变的),对于需要频繁字符串更改的场景,更好的方法是使用 StringBuilder 类
一种很常见的场景是将列表转为字符串,这种情况下可以使用 StringJoiner,此类可以指定字符串的分隔符、开头和结尾,类似于 Python 中列表的 join 方法。对于 String[],其含有使用 StringJoiner 的 join 方法
随机数
使用 Math.random() 可以获得伪随机数,更好的随机数是使用 SecureRandom 来创建安全的、不可预测的随机数
可以使用 SecureRandom.getInstanceStrong() 来获得真随机数,但是如果系统的熵不够(一般出现在 Docker 中),此方法会导致线程阻塞
异常
Java 也是 try-catch-finally 异常块。finally 异常块无论是否抛出异常都会被执行。Java 中异常的分类为:
- graph TD
Throwable -→ Error & Exception Exception -→ RuntimeException & 其它异常
在 Java 中,除了 Error 和 RuntimeException 异常外,其它异常在 main 函数结束前必须被捕获
日志
Java 使用 Commons Logging 作为 日志门面,使用 maven 引入:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
使用示例:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class Study {
public static void main(String[] args) {
Log log = LogFactory.getLog(Study.class);
log.info("start...");
log.warn("end");
}
}
反射
反射用于在运行时查询对象的信息并根据这些信息访问对象成员或调用成员函数。反射可以绕过类的权限控制。之所以会出现这种情况,是因为 JVM 保留了对象的所有信息(数据成员、成员函数、接口、基类等)
对象的信息被保留在了对象的静态数据成员 class 中,此数据成员的类型为 Class,一个类的 class 数据成员是 JVM 唯一的。因此我们可以通过比较两个对象的 class 来判断两个对象是否是同一个类
相比而言,instanceof 判断的是某个对象是否属于某个继承体系(此对象可以安全地转为目标类型)。 |
一个简单的使用方法是:
public class Study { public static void main(String[] args) throws Throwable { Study stu = new Study(); Class cls = stu.getClass(); Class cls2 = Class.forName("java.lang.String"); assert cls != cls2; System.out.println(cls.getName()); } }
获取类的字段:
import java.lang.reflect.Field; public class Study { public int score = 10; private int grade = 20; public static void main(String[] args) throws Throwable { Study stu = new Study(); Class cls = Study.class; Field score = cls.getField("score"); Field grade = cls.getDeclaredField("grade"); System.out.println(score.getInt(stu)); System.out.println(grade.getInt(stu)); } }
调用类的方法:
import java.lang.reflect.Method; public class Study { public void hello(){ System.out.println("hello"); } private void world(){ System.out.println("world"); } public static void main(String[] args) throws Throwable { Study stu = new Study(); Class cls = Study.class; Method hello = cls.getMethod("hello"); Method world = cls.getDeclaredMethod("world"); hello.invoke(stu); world.invoke(stu); } }
既然可以调用类的方法,那么按名创建对象的功能也应当有:
import java.lang.reflect.Constructor; public class Study { public static void main(String[] args) throws Throwable { Class cls = Class.forName("java.lang.String"); Constructor str = cls.getConstructor(); Object obj = str.newInstance(); } }
实际上如果对象的构造函数如果不是私有的,那么可以直接使用 Class.newInstance 创建对象
动态代理
一般而言,interface 是不可实例化的,但是通过动态代理可以在运行时创建某个接口的实例:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; interface Hello{ void world(String str); } public class Study { public static void main(String[] args) throws Throwable { InvocationHandler handler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(method.getName().equals("world")){ System.out.println("hello, " + args[0]); } return null; } }; Hello hello = (Hello) Proxy.newProxyInstance( Hello.class.getClassLoader(), // 传入 ClassLoader new Class[]{Hello.class}, // 传入需要实现的接口数组 handler // 传入 InvocationHandler 实例 ); hello.world("xiaoming"); } }
注解
注解分为三类:
编译器使用的注解:例如 @Override,这些在编译期提供信息,不会进入 .class 文件
工具处理 .class 使用的注解。会进入 .class 但加载后不会进入内存中
程序运行时能够读取的注解,加载后一直存在于 JVM 中
一个注解可以存在参数,参数可以为任意类型,但是必须是常量。如果不提供参数,则相当于使用默认值
@interface Report{ int type() default 0; String level() default "info"; String value () default ""; } public class Study { @Report(type = 10) public static void main(String[] args) throws Throwable { } }
元注解:
元注解可以用于修饰注解。例如:
注解 | 功能 |
@Target | 限定注解应用的位置 |
@Retention | 限定注解的生命周期 |
Repeatable | 定义注解是否可被重复使用 |
Inherited | 定义子类是否可以继承父类的注解 |
泛型
数据结构
Java 提供了一些容器:
- graph TD
Collection -→ List & Queue & Set List -→ Vector & ArrayList & LinkedList Vector -→ Stack Queue -→ LinkedList & A[Priority Queue] Set -→ HashSet & TreeSet HashSet -→ LinkHashSet
graph TD
Map -→ HashMap & TreeMap
和 C++ 不同,Hashtable、Vector、Stack 是历史遗留容器,不应当继续使用。另外,Enumeration 是遗留接口,已被 Iterator 所取代 |
现在,Java 访问集合统一使用迭代器的方式
下面是这些容器的具体信息:
容器名 | 逻辑结构 | 物理结构 |
ArrayList | 列表 | 数组 |
LinkedList | 列表 | 链表 |
Queue | 队列 | |
Priority Queue | 优先队列 | 堆 |
HashSet | 集合 | 散列表 |
TreeSet | 集合 | 红黑树 |
HashMap | 字典 | 散列表 |
TreeMap | 字典 | 红黑树 |