java基础(2) 面向对象编程-java核心类
面向对象
面向对象对应的就是面向过程,
面向过程就是一步一步去操作,你需要知道每一步的步骤。
面向对象的编程以对象为核心,通过定义类描述实体及其行为,并且支持继承、封装和多态等特性
面向对象基础
面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。
现实世界中,我们定义了“人”这种抽象概念,而具体的人则是“小明”、“小红”、“小军”等一个个具体的人。所以,“人”可以定义为一个类(class),而具体的人则是实例(instance):
class Test {
private int field1;
public String field2;
// 构造函数
public Test(String field2, int field1) {
this.field1 = field1;
this.field2 = field2;
}
public int getField1(){
return this.field1;
}
public void setField1(int val){
if(val < 0 || val > 100){
// 抛出错误
throw new IllegalArgumentException("invalid age value");
}
// 赋值
this.field1 = val;
}
};
public class Main {
public static void main(String[] args){
Test t = new Test();
t.setField1(2);
System.out.println(t.getField1());
System.out.println(t.field2);
}
}
写法同js类型,但是需要指定Test类型。一般字段使用private修饰符修饰,表示私有属性,外部无法访问,但是可以通过设置公共的方法去获取和设置值。
java类的构造函数,是public 类名(){},跟js的constructor(){}还是有点区别的。并且java可以写多个构造方法。除此之外,构造方法之间还能相互调用。
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this(name, 18); // 调用另一个构造方法Person(String, int)
}
public Person() {
this("Unnamed"); // 调用另一个构造方法Person(String)
}
}
方法重载
如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。
public void setField1(int val){
if(val < 0 || val > 100){
// 抛出错误
throw new IllegalArgumentException("invalid age value");
}
// 赋值
this.field1 = val;
}
public void setField1(int val, boolean isTrue){
if(val < 0 || val > 100){
// 抛出错误
throw new IllegalArgumentException("invalid age value");
}
// 赋值
this.field1 = val;
}
比如String.indexOf()方法,就提供了多种调用方法。
举个例子,String类提供了多个重载方法indexOf(),可以查找子串:
int indexOf(int ch):根据字符的Unicode码查找;
int indexOf(String str):根据字符串查找;
int indexOf(int ch, int fromIndex):根据字符查找,但指定起始位置;
int indexOf(String str, int fromIndex)根据字符串查找,但指定起始位置。
继承
所有的类都默认继承与object,只有object例外,他没有父类。
java的继承同js差不多,只有一点区别,这里不多详述。
多态
子类可以重写父类的方法。这里需要注意,只有方法名,参数,返回值相同,才算是重写。加上@Overide装饰器可以方便编译器检查报错。
class Person {
public void run() {
System.out.println("Person.run");
}
}
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法;
没运行之前不知道运行的是父类的方法还是子类的方法。
如果父类不想子类继承某个方法,可以用final修饰方法。
class Person {
protected String name;
public final String hello() {
return "Hello, " + name;
}
}
用final修饰的类不允许被继承
用final修饰的属性,不允许被重新赋值
抽象类
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:
abstract class Person {
public abstract void run();
}
包含抽象方法的类不允许被实例化。抽象类强迫子类必须实现他的抽象方法。
用法跟ts类似,这里不多详述
面向抽象编程的本质就是:
- 上层代码只定义规范(例如:abstract class Person);
- 不需要子类就可以实现业务逻辑(正常编译);
- 具体的业务逻辑由不同的子类实现,调用者并不关心。
接口
没有字段的,且全部方位都是抽象方法的抽象类,就可以改写为接口。
abstract class Person {
public abstract void run();
public abstract String getName();
}
//改写为
interface Person {
void run();
String getName();
}
接口定义的所有方法默认都是public abstract的
而当一个具体的类想去实现一个接口的时候,就需要使用implements,而非extends。
一个类只能继承于一个类,但是一个类可以implements多个接口。
接口之间也可以通过extends去继承。
接口还可以定义一个非抽象方法default,这个不强求类去实现,目的是为了:
当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
静态字段和静态方法
实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。举个例子:
class Person {
public String name;
public int age;
// 定义静态字段number:
public static int number;
}
对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例;
可以把静态字段立即为类自己的属性,一般通过类.字段去获取,而不是通过实例.字段去获取。
同理,静态方法也一致,属于类,可以直接通过类.方法名去调用,但是内部无法访问this。
静态方法经常用于工具类。例如:
Arrays.sort()
Math.random()
包
在Java中,我们使用package来解决名字冲突。
Java定义了一种名字空间,称之为包:package。一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名。
类似于ts的namespace,防止命名冲突。
JDK的Arrays类存放在包java.util下面,因此,完整类名是java.util.Arrays。
包的命名跟文件存放位置也有关。
导入其他包
- 直接写完整的包名:
lin.TestClass
- 先在当前包
import lin.TestClass
,就可以直接用了new TestClass
- 也可以直接import lin.*,他会将lin包的class引入。
- 默认引入
java.lang
的包,比如String, 这些。
作用域
比如public、protected、private,这些修饰符,用来限定作用域的范围。
publci的类,可以被其他包访问。public的属性和方法,也可以被其他类访问。
非publci的类,外部无法访问,就跟js没有export一样
private定义的方法和属性,无法在类外被访问。
由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权限:
public class Main {
public static void main(String[] args) {
Inner i = new Inner();
i.hi();
}
// private方法:
private static void hello() {
System.out.println("private hello!");
}
// 静态内部类:
static class Inner {
public void hi() {
Main.hello();
}
}
}
private定义的字段和方法,其继承类中无法使用,procted修饰的方法和字段,可以在子类中使用,但同样不可以被实例调用。
package权限(包作用域)
最后,包作用域是指一个类允许访问同一个package的没有private修饰的class,以及没有protected、private修饰的字段和方法。
final
- 用final修饰class可以阻止被继承:
- 用final修饰method可以阻止被子类覆写
- 用final修饰字段可以阻止被重新赋值
- 用final修饰局部变量可以阻止被重新赋值
内部类
也是嵌套类。
classpath和jar
classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
。
现在我们假设classpath是.;C:\work\project1\bin;C:\shared
,当JVM在加载abc.xyz.Hello
这个类时,会依次查找:
- <当前目录>\abc\xyz\Hello.class
- C:\work\project1\bin\abc\xyz\Hello.class
- C:\shared\abc\xyz\Hello.class
在系统环境变量中设置classpath环境变量,不推荐;
在启动JVM时设置classpath变量,推荐。
启动jvm的时候🌽定义
java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello
jar包
如果有很多.class文件,散落在各层目录中,肯定不便于管理。如果能把目录打一个包,变成一个文件,就方便多了。
jar包就是用来干这个事的,它可以把package组织的目录层级,以及各个目录下的所有文件(包括.class文件和其他文件)都打成一个jar文件,这样一来,无论是备份,还是发给客户,就简单多了。
jar包相当于目录,可以包含很多.class文件,方便下载和使用;
MANIFEST.MF文件可以提供jar包的信息,如Main-Class,这样可以直接运行jar包。
java核心类
字符串
实际上字符串在String内部是通过一个char[]数组表示的,因此,按下面的写法也是可以的:
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的private final char[]字段,以及没有任何修改char[]的方法实现的。
不可变不是说变量s2不可变,s2可以重新赋值一个新的String对象,只不过原有的String对象不会改变。
字符串比较
java拥有一个字符串常量池,如果多个字符串具有相同的值,那么他们将共享存储在常量池中的同一个实例,也就是说
String s = "hello"
,这个hello,实际上是存放在常量池中,s存放的值是hello的地址,类似于js的对象,只不过js的字符串是基础类型而不是引用类型。
当我们想要比较两个字符串是否相同时,要特别注意,我们实际上是想比较字符串的内容是否相同。必须使用equals()方法而不能用==
因为==实际上比较的是内存地址,
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
==返回false,只有equals才返回true。
其他字符串方法同js类似
"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"
" \tHello\r\n ".trim(); // "Hello" 返回的Hello是新的String对象
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
//字符串占位
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));
字符串转换
要把任意基本类型或引用类型转换为字符串,可以使用静态方法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
// char 和 String 互转
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String
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编码转换
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
小结
- Java字符串String是不可变对象;
- 字符串操作不改变原字符串内容,而是返回新字符串;
- 常用的字符串操作:提取子串、查找、替换、大小写转换等;
- Java使用Unicode编码表示String和char;
- 转换编码就是将String和byte[]转换,需要指定编码;
- 转换为byte[]时,始终优先考虑UTF-8编码。
StringBuilder
Java编译器对String做了特殊处理,使得我们可以直接用+拼接字符串
但是
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
上面循环代码,每次循环都会创建新的String对象,而老的String对象则被丢弃,造成内存浪费,所以java提供StringBuilder来高校拼接String;
Java标准库提供了StringBuilder,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder中新增字符时,不会创建新的临时对象:
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',').append(i);
}
String s = sb.toString();
注意:对于普通的字符串+操作,并不需要我们将其改写为StringBuilder,因为Java编译器在编译时就自动把多个连续的+操作编码为StringConcatFactory的操作。在运行期,StringConcatFactory会自动把字符串连接操作优化为数组复制或者StringBuilder操作。
小结
- StringBuilder是可变对象,用来高效拼接字符串;
- StringBuilder可以支持链式操作,实现链式操作的关键是返回实例本身;
StringJoiner
类似用分隔符拼接数组的需求很常见,所以Java标准库还提供了一个StringJoiner来干这个事:
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ", "Hello ", "!");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());
}
}
第一个参数指定拼接的字符串,第二个参数指定开头,第三个参数指定结尾。
String还提供了一个静态方法join(),这个方法在内部使用了StringJoiner来拼接字符串,在不需要指定“开头”和“结尾”的时候
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
包装类型
Integer类实现,
public class Integer {
private int value;
public Integer(int value) {
this.value = value;
}
public int intValue() {
return this.value;
}
}
有点像js中,string对应的String, number对应的Numer,
int 和Integer可以互相转换
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
所有的包装类都是不变类,
public final class Integer {
private final int value;
}
init在初始化赋值之后,将不能再修改;
对两个Integer实例进行比较要特别注意:绝对不能用==比较,因为Integer是引用类型,必须使用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比较是相等的,这是因为,在Integer内部,为了优化性能,对于较小的,Integer.initValue总是返回同一个实例,这也导致==为true。
Integer x = 127
//等同于
Integer x = Integer.valueOf(127)
Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
创建新对象时,优先选用静态工厂方法而不是new操作符。
进制转换
Integer
本身还提供了很多方法,就跟js的Number.xxx
一样,
- parseInt 将其他类型转为Integer类型
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
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进制
Java的包装类型还定义了一些有用的静态变量
// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;
// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647 类似于js的x`
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();
JavaBean
符合以下这种
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)
读写方法名分别以get和set开头,并且后接大写字母开头的字段名Xyz,因此两个读写方法名分别是getXyz()和setXyz()。
类似于js的defineProperty的get set方法。
JavaBean是一种符合命名规范的class,它通过getter和setter来定义属性;
属性是一种通用的叫法,并非Java语法规定;
可以利用IDE快速生成getter和setter;
使用Introspector.getBeanInfo()可以获取属性列表。
枚举类
在Java中,我们可以通过static final
来定义常量
public class Weekday {
public static final int SUN = 0;
public static final int MON = 1;
public static final int TUE = 2;
public static final int WED = 3;
public static final int THU = 4;
public static final int FRI = 5;
public static final int SAT = 6;
}
enum
为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum来定义枚举类:
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
有点类型于ts的enum,
- 不同类型的枚举,不能相互赋值。
- 不同类型的值,不可以直接==
Test1 tt = Test1.TEST_1;
tt = HAHA.HAHA_1;
int test = 1;
test == Test1.TEST_1;
enum Test1 { TEST_1, TEST_2 };
enum HAHA {HAHA_1, HAHA_2};
enum实际上创建的也是一个class
- 定义的enum类型总是继承自java.lang.Enum,且无法被继承;
- 只能定义出enum的实例,而无法通过new操作符创建enum的实例;
- 定义的每个实例都是引用类型的唯一实例;
- 可以将enum类型用于switch语句。
public enum Color {
RED, GREEN, BLUE;
}
//实际上
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类和普通class并没有任何区别。但是我们自己无法按定义普通class那样来定义enum,必须使用enum关键字,这是Java语法规定的。
因为enum是一个class,每个枚举的值都是class实例,因此,这些实例有一些方法:name()
String s = Weekday.SUN.name(); // "SUN"
int n = Weekday.MON.ordinal(); // 1 返回定义的常量的顺序,从0开始计数
因为enum实际上定义的也是类,所以可以根据构造函数,添加每个实例对应的属性。比如
enum Weekday {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
public final int dayValue;
private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
MON(1)
相当于执行new操作,将1传入作为dataValue的值,这样就可以通过.dataValue
去获取值。
再比如
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;
}
}
还可以多用其他属性,然后重新toString方法。name属于final,无法被重写。
小结
- Java使用enum定义枚举类型,它被编译器编译为final class Xxx extends Enum { … };
- 通过name()获取常量定义的字符串,注意不要使用toString();
- 通过ordinal()返回常量定义的顺序(无实质意义);
- 可以为enum编写构造方法、字段和方法
- enum的构造方法要声明为private,字段强烈建议声明为final;
- enum适合用在switch语句中。
BigInteger BigDecimal
整数最大是long类型,超过了的话,java.math.BigInteger就是用来表示任意大小的整数。BigInteger内部用一个int[]数组来模拟一个非常大的整数:
BigInteger bi = new BigInteger("1234567890");
System.out.println(bi.pow(5)); // 2867971860299718107233761438093672048294900000
和BigInteger类似,BigDecimal可以表示一个任意大小且精度完全准确的浮点数。
实际上一个BigDecimal是通过一个BigInteger和一个scale来表示的,即BigInteger表示一个完整的整数,而scale表示小数位数
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}
常用工具类
Math
顾名思义,Math类就是用来进行数学计算的,它提供了大量的静态方法来便于我们实现数学计算:
Math.abs(-13) //13 绝对值
Math.max(13,9) //13 最大
Math.min() //最小
Math.pow(2, 10); // 2的10次方=1024
double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.7182818...
Math.sin(Math.PI / 6); // sin(π/6) = 0.5
Math.random(); // 0.53907... 每次都不一样
Java标准库还提供了一个StrictMath,它提供了和Math几乎一模一样的方法。这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如x86和ARM)计算的结果可能不一致(指误差不同),因此,StrictMath保证所有平台计算结果都是完全相同的,而Math会尽量针对平台优化计算速度,所以,绝大多数情况下,使用Math就足够了。
·
Random
生成伪随机数
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
SecureRandom
有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom就是用来创建安全的随机数的:
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));
SecureRandom
的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。