Java基础
一、基本数据类型
- 整数类型:
byte,short,int,long - 浮点数类型:float,double
- 字符类型:char
- 布尔类型:boolean
各种整型能表示的最大范围:
- byte:-128 ~ 127(2⁷ − 1)
- short: -32768 ~ 32767(2¹⁵ − 1)
- int: -2147483648 ~ 2147483647(2³¹ − 1)
- long: -9223372036854775808 ~ 9223372036854775807(2⁶³ − 1)
// 定义整型
public class Main {
public static void main(String[] args) {
int i = 2147483647;
int i2 = -2147483648;
int i3 = 2_000_000_000; // 加下划线更容易识别
int i4 = 0xff0000; // 十六进制表示的16711680
int i5 = 0b1000000000; // 二进制表示的512
long n1 = 9000000000000000000L; // long型的结尾需要加L
long n2 = 900; // 没有加L,此处900为int,但int类型可以赋值给long
int i6 = 900L; // 错误:不能把long型赋值给int
}
}
浮点数能表示的范围虽大,但其精度是有限的。float 通常能保证约 6-7 位有效十进制数字,double 则能保证约 15-16 位
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
除了上述基本类型的变量,剩下的都是引用类型
二、位运算
<<运算会将一个数的二进制左移补0>>运算会将一个数的二进制按照符号右移,正数补0,负数补1>>>运算会将一个数的二进制右移补0,不分符号,这意味着会把负数移位成正数- 此外,还提供了常规的按位布尔运算
& | ~ ^
- 此外,还提供了常规的按位布尔运算
三、作用域
| 访问修饰符 | 同一个类 | 同一个包 | 不同包的子类 | 任何地方 |
|---|---|---|---|---|
| private | ✅ | ❌ | ❌ | ❌ |
| default(无修饰符) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
1.public 类 — 完全公开
// 文件:com/example/Utils.java
package com.example;
public class Utils {
// 任何地方都能访问这个类
}
2. default类 — 仅包内可见
// 文件:com/example/internal/Helper.java
package com.example.internal;
class Helper { // 无 public,包内可见
// 只能被 com.example.internal 下的类访问
}
四、模块
| 命令 | 作用 | 产物 |
|---|---|---|
javac |
编译源代码 | .class 字节码 |
jar |
打包/分发 | .jar 可执行包 |
jmod |
转为模块格式 | .jmod(给 jlink 用) |
jlink |
裁剪 JDK,制作定制 JRE | 迷你 JRE 文件夹 |
javac 编译,jar 分发,jlink 裁剪——普通开发用前两个,做精简部署才需要第三个。
五、枚举类
如果希望定义一些枚举常量,可以使用枚举类enum
public enum Color {
RED, GREEN, BLUE;
}
每个变量都是一个Color类型的实例
实际上,编译器编译出的class大概就像这样:
public final class Color extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}
- 定义的每个实例都是引用类型的唯一实例,因此可以使用
==进行比较 - 可以将
enum类型用于switch语句
这些实例有一些常用方法
-
name()返回常量名,例如:
java String s = Weekday.SUN.name(); // "SUN" -
ordinal()返回定义的常量的顺序,从0开始计数,例如:
java int n = Weekday.MON.ordinal(); // 1改变枚举常量定义的顺序就会导致
ordinal()返回值发生变化。
实际上,可以自行编写构造方法和实例方法
enum Weekday {
MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");
public final int dayValue;
private final String chinese;
private Weekday(int dayValue, String chinese) {
this.dayValue = dayValue;
this.chinese = chinese;
}
@Override
public String toString() {
return this.chinese;
}
}
并且enum类天然适配switch
六、记录类
如果希望定义一个类,有若干不变量private final ,可以使用记录类record
record Point(int x, int y) {}
对于自定义类,需要覆写equals()和hashCode()方法。而记录类替我们完成了
相当于以下代码:
final class Point extends Record {
private final int x;
private final int y;
//原始构造方法
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return this.x;
}
public int y() {
return this.y;
}
public String toString() {
return String.format("Point[x=%s, y=%s]", x, y);
}
public boolean equals(Object o) {
...
}
public int hashCode() {
...
}
}
编译器自动创建了构造方法,和字段名同名的方法,以及覆写toString()、equals()和hashCode()方法。
也可以自行给记录类写构造方法和静态方法
七、常用工具类
1. Math
Math类提供了大量的静态方法便于实现数学计算
求绝对值:
Math.abs(-100); // 100
Math.abs(-7.8); // 7.8
取最大或最小值:
Math.max(100, 99); // 100
Math.min(1.2, 2.3); // 1.2
计算xy次方:
Math.pow(2, 10); // 2的10次方=1024
计算 xx:
Math.sqrt(2); // 1.414...
计算ex次方:
Math.exp(2); // 7.389...
计算以e为底的对数:
Math.log(4); // 1.386...
计算以10为底的对数:
Math.log10(100); // 2
三角函数:
Math.sin(3.14); // 0.00159...
Math.cos(3.14); // -0.9999...
Math.tan(3.14); // -0.0015...
Math.asin(1.0); // 1.57079...
Math.acos(1.0); // 0.0
Math还提供了几个数学常量:
double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.7182818...
Math.sin(Math.PI / 6); // sin(π/6) = 0.5
生成一个随机数x,x的范围是0 <= x < 1:
Math.random(); // 0.53907... 每次都不一样
2. BigInteger
long基本类型提供最大2^64-1 。如果需要计算的数超过此界限,则需要使用BigInteger 。
BigInteger 采用数组模拟大数,这让它可以模拟任意大的数。缺点是速度较慢
- 对
BigInteger进行运算,只能用它提供的实例方法 -
可以把
BigInteger转化为基本类型- 转换为
long:longValue() - 转换为
byte:byteValue() - 转换为
short:shortValue() - 转换为
int:intValue() - 转换为
long:longValue() - 转换为
float:floatValue() - 转换为
double:doubleValue()
转型时超过会丢失高位信息
而使用
longValueExact()等方法转型,则会抛出ArithmeticException - 转换为
3. BigDecimal
和BigInteger类似,BigDecimal可以表示一个任意大小且精度完全准确的浮点数。BigDecimal也是从Number继承的,也是不可变对象。
BigDecimal bd = new BigDecimal("123.4567");
System.out.println(bd.multiply(bd)); // 15241.55677489
- 实例方法
scale()表示小数位数。返回负数,例如-2,则表示这个数是个整数,并且末尾有2个0。 stripTrailingZeros()方法,可以将一个BigDecimal格式化为一个相等的,但去掉了末尾0的BigDecimal-
setScale()方法可以设置小数位数java BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四舍五入,123.4568 BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN); // 直接截断,123.4567 System.out.println(d2);
对BigDecimal做加减乘运算,精度不会丢失,但是做除法,如果出现无法除尽的情况,就必须指定精度以及如何进行截断
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("23.456789");
BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保留10位小数并四舍五入
BigDecimal d4 = d1.divide(d2); // 报错:ArithmeticException,因为除不尽
在比较两个BigDecimal的值是否相等时,要特别注意,使用equals()方法不但要求两个BigDecimal的值相等,还要求它们的scale()相等:
| 方法 | 比较内容 |
|---|---|
compareTo() |
数值大小(忽略末尾的 0) |
equals() |
数值大小 + 精度(小数位数) |
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("123.45600");
System.out.println(d1.equals(d2)); // false,因为scale不同
System.out.println(d1.equals(d2.stripTrailingZeros())); // true,因为d2去除尾部0后scale变为3
System.out.println(d1.compareTo(d2)); // 0 = 相等, -1 = d1 < d2, 1 = d1 > d2
八、异常
Java标准库定义的常用异常包括:
Exception
├─ RuntimeException // 非受检异常,不强制要求捕获
│ ├─ NullPointerException // 在一个null上调用方法或者访问属性
│ ├─ IndexOutOfBoundsException // 访问数组,字符串或集合索引<0或>=长度
│ ├─ SecurityException //
│ └─ IllegalArgumentException // 调用方法时,传入的参数不符合方法内部的约定
│ └─ NumberFormatException // 试图将字符串转换为数值,但字符串内容不是合法数字
├─ IOException // 受检异常,强制要求捕获
│ ├─ UnsupportedCharsetException // 使用了JVM不认识的字符编码名称。
│ ├─ FileNotFoundException // 试图打开不存在的文件进行操作
│ └─ SocketException // 通常是网络无法连接、连接被重置(对方突然断开)或网络不可达。
├─ ParseException // 文本解析失败
├─ GeneralSecurityException // 加密解密、数字签名等操作出错
├─ SQLException // 访问数据库时发生错误
└─ TimeoutException // 多线程或异步操作等待结果时,超过了设定的最大等待时间
对于大型项目,可以自定义异常,但是一般JDK已定义的异常就足够
详见
Commons Logging
Commons Logging是一个第三方日志库,由Apache创建的日志模块。
依赖:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.4.0</version>
</dependency>
创建log实例
Log log = LogFactory.getLog(Main.class);
// 可以传入类,也可以传入字符串,指定该日志的名称
// 传入getClass()可以方便的获取当前类名
Commons Logging定义了6个日志级别:
- FATAL
- ERROR
- WARNING
- INFO
- DEBUG
- TRACE
默认级别是INFO。INFO以下的默认不会被打印
常用log.error(String)和log.error(String, Throwable)打印异常。
九、反射
JVM为每个加载的class以及interface创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等
三种方法获取一个类的Class实例
-
直接通过一个
class的静态变量class获取:java Class cls = String.class; -
通过某实例变量提供的
getClass()方法获取:java String s = "Hello"; Class cls = s.getClass(); -
如果知道一个
class的完整类名,可以通过静态方法Class.forName()获取:java Class cls = Class.forName("java.lang.String");
每个Class实例在JVM中是唯一的
// 获取一个Class的信息
static void printClassInfo(Class cls) {
System.out.println("Class name: " + cls.getName());
System.out.println("Simple name: " + cls.getSimpleName());
if (cls.getPackage() != null) {
System.out.println("Package name: " + cls.getPackage().getName());
}
System.out.println("is interface: " + cls.isInterface());
System.out.println("is enum: " + cls.isEnum());
System.out.println("is array: " + cls.isArray());
System.out.println("is primitive: " + cls.isPrimitive());
}
如果获取到了一个Class实例,我们就可以通过该Class实例来创建对应类型的实例:
// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();
局限是只能调用public的无参数构造方法
1. 获取字段
getField方法获取public字段
getDeclaredField方法获取private和protected 字段
对于非public字段,获取之后还需使用setAccessible(true)方法设置权限
获取到字段实例之后,可以使用get和set方法获取或修改值,get方法传入需要修改的实例对象,set传入对象和新值
package org.example;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
Person p=new Student(1,2);
Class cls=p.getClass();
// 获取字段
Field scoreField=cls.getField("score");
Field gradeField=cls.getDeclaredField("grade");
gradeField.setAccessible(true);
gradeField.set(p,3);
System.out.println(scoreField.get(p));
System.out.println(gradeField.get(p));
}
}
class Student extends Person {
public int score;
private int grade;
public Student(int score,int grade){
this.score=score;
this.grade=grade;
}
}
class Person {
public String name;
}
2. 获取方法
同样的,可以通过Class实例获取所有Method信息。
-
Method getMethod(name, Class...)获取某个
public的Method,包括父类。 -
Method getDeclaredMethod(name, Class...)获取当前类的某个
Method,不包括父类。可获取private方法 -
Method[] getMethods()获取所有
public的Method,包括父类。注意所有类都至少有一个父类Object -
Method[] getDeclaredMethods()获取当前类的所有
Method,不包括父类,包括private方法
以上方法都传入需要获取的方法的方法名(字符串),以及对应参数的的CLass 类。
重载方法就是按照传入不同参数的CLass类区分。
同样的,对于非public方法,需要setAccessible(true) 设置权限。
一个Method对象包含该方法的所有信息
getName():返回方法名称,例如:"getScore";getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class;getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。getParameterCount():返回方法的参数个数
获取到方法之后,使用invoke()方法可以执行该方法。传入所要执行方法的实例和对应参数。
如果获取的方法是静态方法,则第一个参数传入null而非所要执行方法的实例
package org.example;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Student p=new Student();
Class cls=p.getClass();
// 测试public
Method m1=cls.getMethod("publicF");
m1.invoke(p);
// 测试重载public
Method m11=cls.getMethod("publicF",String.class);
m11.invoke(p,"hello");
// 测试private和protected
Method m2=cls.getDeclaredMethod("privateF");
m2.setAccessible(true);
m2.invoke(p);
Method m3=cls.getDeclaredMethod("protectedF");
m3.setAccessible(true);
m3.invoke(p);
System.out.println("==========");
// 测试多方法获取
Method[] ms=cls.getDeclaredMethods();
for (Method m:ms){
m.setAccessible(true); // 处理多方法获取到的非public方法
if(m.getParameterCount()==0){ // 处理无参方法
m.invoke(p);
} else { // 有参方法
m.invoke(p,"hello world");
}
}
}
}
class Student{
public Student(){}
// public方法
public void publicF(){
System.out.println("publicF");
}
// 重载public方法
public void publicF(String s){
System.out.println(s);
}
// 多个public方法
public void publicG(){
System.out.println("publicG");
}
// private方法
private void privateF(){
System.out.println("privateF");
}
// protected方法
protected void protectedF(){
System.out.println("protectedF");
}
}
publicF
hello
privateF
protectedF
==========
publicG
hello world
publicF
protectedF
privateF
反射获取的方法的调用依旧遵循多态
3. 构造方法
前文提到过newInstance()构造有局限。为此使用Constructor对象。
通过Class实例获取Constructor的方法如下:
getConstructor(Class...):获取某个public的Constructor;getDeclaredConstructor(Class...):获取某个Constructor;
上述两个方法均传入该构造所需的参数类型的Class类。
不同于普通方法需要首先传入方法名,构造方法只由参数数量类型区分,所以无需传入名。
getConstructors():获取所有public的Constructor;getDeclaredConstructors():获取所有Constructor。
Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
同样,非public的Constructor时,需要setAccessible(true)设置允许访问。
获取Constructor 实例后,使用newInstance() 方法即可构造新实例,传入相应的参数,并且前置转型。
4. 获取接口
有了Class实例,就可获取它的父类的Class ,使用getSuperclass()方法。
除Object类的父类是null外,其他所有类都有父类。
一个类可能实现一个或多个接口,使用getInterfaces() 方法获取该类(不包含父类)实现的所有接口。
返回的接口也是Class类型
package org.example;
public class Main {
public static void main(String[] args) throws Exception {
Calculator calculator=new Calculator(1,2);
Class cls=calculator.getClass();
Class[] interfazes=cls.getInterfaces();
for (Class interfaze:interfazes){
System.out.println(interfaze);
System.out.println(interfaze.isInterface());
}
}
}
interface Calculator1{
int add(int a,int b);
int sub(int a,int b);
}
interface Calculator2{
int mul(int a,int b);
int div(int a,int b);
}
class Calculator implements Calculator1,Calculator2{
public int a;
public int b;
public Calculator(){}
public Calculator(int a,int b){
this.a=a;
this.b=b;
}
@Override
public int add(int a, int b) {return 0;}
@Override
public int sub(int a, int b) {return 0;}
@Override
public int mul(int a, int b) {return 0;}
@Override
public int div(int a, int b) {return 0;}
}
interface org.example.Calculator1
true
interface org.example.Calculator2
true
十、注解
1. 元注解
元注解用于修饰注解,自定义注解的时候会用到。通常不需要自己去编写元注解
@Target
@Target传入的参数定义了Annotation能够被应用于源码的哪些位置
- 类或接口:
ElementType.TYPE; - 字段:
ElementType.FIELD; - 方法:
ElementType.METHOD; - 构造方法:
ElementType.CONSTRUCTOR; - 方法参数:
ElementType.PARAMETER。
如果需要定义在多个地方,可以传入数组{ElementType.METHOD, ElementType.FIELD }
实际上@Target定义的value是ElementType[]数组,只有一个元素时,可以省略数组的写法。
@Retention
@Retention定义了Annotation的生命周期:
- 仅编译期:
RetentionPolicy.SOURCE; - 仅class文件:
RetentionPolicy.CLASS; - 运行期:
RetentionPolicy.RUNTIME
Annotation默认为CLASS。但自定义的Annotation通常是RUNTIME,所以务必加上@Retention(RetentionPolicy.RUNTIME)
@Repeatable
@Repeatable定义了Annotation可否在一个地方重复声明。
要使用@Repeatable ,首先要定义一个专门的容器
// 1. 定义可重复注解,@Repeatable 的参数是 Schedules.class
@Repeatable(Schedules.class)
public @interface Schedule {
...
}
// 2. 定义容器注解 Schedules,它必须有一个 value() 方法,
// 返回类型是 Schedule[] (可重复注解的数组)
public @interface Schedules {
Schedule[] value(); // 这是必需的
}
容器注解没有实质用处,专门为了定义@Repeatable 注解服务
@Inherited
@Inherited定义子类是否可继承父类定义的Annotation 。仅针对@Target(ElementType.TYPE)类型有效。
@Inherited 不需要传入参数。
@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
在使用的时候,如果一个类用到了@Report:
@Report(type=1)
public class Person {
}
则它的子类默认也定义了该注解:
public class Student extends Person {
}
2. 定义注解
-
用
@interface语法定义注解:java public @interface Report { } -
添加参数、默认值(强烈推荐)
jsx public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }最常用的参数是
value -
添加元注解
jsx @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
3. 获取注解
注解有何作用完全由人为定义决定。使用反射读取注解信息
判断某个注解是否存在于Class、Field、Method或Constructor:
Class.isAnnotationPresent(Class)Field.isAnnotationPresent(Class)Method.isAnnotationPresent(Class)Constructor.isAnnotationPresent(Class)
以上方法传入的参数是自定义注解的Class
// 判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);
判断是否存在之后,就可以读取Annotation:
Class.getAnnotation(Class)Field.getAnnotation(Class)Method.getAnnotation(Class)Constructor.getAnnotation(Class)
传入注解Class,返回注解类型实例。如果没有经过存在检测就获取,并且没有此注解,则返回null 。
上述没有提到方法参数注解的获取。这是因为一个方法可有多个参数,每个参数又可存在多个注解,所以获取方法参数的注解需要用一个二维数组。
public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}
// 多参数,且参数可有多注解
// 获取方法参数的注解步骤
// 1.获取Method实例:
Method m = ...
// 2.获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range r) { // @Range注解
r.max();
}
if (anno instanceof NotNull n) { // @NotNull注解
//
}
}
4. 使用注解
@Range注解,我们希望用它来定义一个String字段的规则:字段长度满足@Range的参数定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) // 用于字段
public @interface Range {
int min() default 0;
int max() default 255;
}
在某个JavaBean中使用该注解:
public class Person {
@Range(min=1, max=20)
public String name;
@Range(max=10)
public String city;
}
但只是定义和使用了注解,目前对程序还无影响,还要自行编写注解逻辑。
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
// 遍历所有Field:
for (Field field : person.getClass().getFields()) {
// 获取Field定义的@Range:
Range range = field.getAnnotation(Range.class);
// 如果@Range存在:
if (range != null) {
// 获取Field的值:
Object value = field.get(person);
// 如果值是String:
if (value instanceof String s) {
// 判断值是否满足@Range的min/max:
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}
本文由 tazume-sans 原创,转载请注明出处。