# 前言
为什么写这样一篇呢,其实学习 java 相关的反序列化也有一段时间了,开始我学习感觉到很困难,我以为是刚开始接触的问题,但是学习一段时间后,我发现或许有这方面的问题,但是绝对不是主要因素,现在我是即将步入大三的学习阶段,java 这门语言还是我大一下学习的一门课,当时因为疫情以及我自己的种种原因,对于 java 基础并不是掌握的很好,也可能是老师对我们比较 “仁慈”,期末考试也是幸运的没有挂科,一年多的时间后的我再拿起这门语言,妄想直接去学习相关的漏洞,但是我对于 java 这个语言的了解,或者说掌握程度并不能支撑我的 “妄想”,陌生的语言模式和大量的代码量让我觉得学习新的内容十分困难,所以这里也是为了打牢基础,或许这个才是我这个假期的主旋律,重温,,,应该是 java 入门之旅或许现在才开始,本篇中所有代码都在 Clown_java/src/main/java/JavaSE at master・clown-q/Clown_java (github.com)
# 面向对象(一)
当然这里我也不会再面向过程的路上花费太多时间,也是直接从 java 的一大重要特性,面向对象开始
# 类和对象(概念)
类是什么?
一个最简单的例子 -- 人类,说白了是对人这种生物的一个定义,无论每个人的个体有什么不一样
对象是什么?
当然这里并不是指你的伴侣,对象是指类的个体,我们每个人都是人类中的一个个体
总的来说,类是一个抽象出来的概念,对象是类中的个体,在 java 中定义一个类,然后创建类的实际对象就是面向对象编程
# 类和对象(实例)
既然现在准备重头开始,当然不会只是记录概念性的问题,我会向一个初学者一样,自己去敲好每一个代码块,上面是用人类来说明类和对象的概念,那么这里也建立一个 Person 类来做一个示例
public class PersonTest { | |
String name; | |
int age; | |
String nickname; | |
} |
向上图这样 class 定义一个类,其中的 name,age,nickname 其实就是人类的属性,类似于 C 语言很明显现在的各个属性都没有赋值,这就是将人抽象出来人类,那么怎么具体指某一个人呢?
创建一个实例来对应一个具体的人
public class testMain { | |
public static void main(String[] args) { | |
new PersonTest(); | |
} | |
} |
使用 new 关键字来创建了一个对象,只不过现在没有赋值,也就没有名字等信息,算是一个 “黑户”,现在给与他一个身份
public class testMain { | |
public static void main(String[] args) { | |
PersonTest personTest = new PersonTest(); | |
personTest.name="小明"; | |
personTest.age=20; | |
personTest.nickname="Clown"; | |
} | |
} |
这个 personTest 是对对象的引用,有了对对象的引用后就可以对属性进行一些操作
形象点理解,这个 PersonTest 是整个 “人类池” 所有人的初始都是空白的,我们从其中 “取” 一个人,给他附魔命名(可能不是恰当,但是或许这样呢个让人更容易理解这个过程)
# 方法(创建使用)
前面简单的说明了一下什么是类,什么是对象,现在我们能够确定某个人,但是也只是给与这个人一些属性,必不可少的还要有一些功能,好比人需要吃饭,可以说话,在 java 中可以用方法来实现,也就是我们常说的函数
public class PersonTest { | |
String name; | |
int age; | |
String nickname; | |
public void eatTest(){ | |
System.out.println("干饭人努力干饭"); | |
} | |
} |
这里就定义了一个函数输出一句话,干饭人努力干饭
public class testMain { | |
public static void main(String[] args) { | |
PersonTest personTest = new PersonTest(); | |
personTest.eatTest(); | |
} | |
} |
这边调用一下后可以看到控制台就输出了一句话,吃饭不用说就会去吃,还有些事是有一定的条件下才会发生的,也就是带有参数的函数
public class PersonTest { | |
String name; | |
int age; | |
String nickname; | |
public void eatTest(){ | |
System.out.println("干饭人努力干饭"); | |
} | |
public void changeName(String name){ | |
this.name=name; | |
} | |
} |
比如说改名,这是要给这个人一个新的名字他才会改名
this 关键字表示的是当前这个对象的属性
public class testMain { | |
public static void main(String[] args) { | |
PersonTest personTest = new PersonTest(); | |
personTest.name="小明"; | |
System.out.println("改名前:"+personTest.name); | |
personTest.changeName("小王"); | |
System.out.println("改名后:"+personTest.name); | |
} | |
} |
这个还是很好理解的
# 方法的重载
还是上面的例子
public class PersonTest { | |
String name; | |
int age; | |
String nickname; | |
public void eatTest(){ | |
System.out.println("干饭人努力干饭"); | |
} | |
} |
这样一个方法,但是如果想要自爆家门来凸显气势的时候,仅仅这样一个方法是不够的,这时候重载也就利用上了
public class PersonTest { | |
String name; | |
int age; | |
String nickname; | |
public void eatTest(){ | |
System.out.println("干饭人努力干饭"); | |
} | |
public void eatTest(String name){ | |
System.out.println("我"+this.name+"干饭人努力干饭"); | |
} | |
} |
如果没有给参数就调用第一个方法,如果给参数就调用第二个方法
public class testMain { | |
public static void main(String[] args) { | |
PersonTest personTest = new PersonTest(); | |
personTest.name="王腾"; | |
personTest.eatTest(); | |
personTest.eatTest(personTest.name); | |
} | |
} |
# 构造方法
一种很特殊的方法,前面我们使用 new 创建一个对象的时候,各个属性其实都是默认值
public class testMain { | |
public static void main(String[] args) { | |
PersonTest personTest = new PersonTest(); | |
System.out.println(personTest.name); | |
} | |
} |
如果想要在对象实例化的时候就给与一个默认的值就需要使用到构造方法了,实际上每个类都会有一个构造方法,当创建对象的时候会自动调用
public class PersonTest { | |
String name; | |
int age; | |
String nickname; | |
PersonTest(){ | |
System.out.println("构造函数执行"); | |
} | |
public void eatTest(){ | |
System.out.println("干饭人努力干饭"); | |
} | |
public void eatTest(String name){ | |
System.out.println("我"+name+"干饭人努力干饭"); | |
} | |
public void changeName(String name){ | |
this.name=name; | |
} | |
} |
然后再 main 中创建对象
public class testMain { | |
public static void main(String[] args) { | |
PersonTest personTest = new PersonTest(); | |
System.out.println(personTest.name); | |
} | |
} |
# 静态变量 & 静态方法
前面提到的方法变量,都属于是成员变量,成员方法,我们想要对其进行操作需要有一个实例引用,在每个对象之间是不共享的,而静态变量静态方法是属于类的,所有的对象共享,使用关键字 static 可以声明一个变量或者一个方法为静态的
public class PersonTest { | |
String name; | |
int age; | |
String nickname; | |
static String test; | |
PersonTest(){ | |
System.out.println("构造函数执行"); | |
} | |
public void eatTest(){ | |
System.out.println("干饭人努力干饭"); | |
} | |
public void eatTest(String name){ | |
System.out.println("我"+name+"干饭人努力干饭"); | |
} | |
public void changeName(String name){ | |
this.name=name; | |
} | |
} |
这里声明 test 为一个静态变量
public class testMain { | |
public static void main(String[] args) { | |
PersonTest personTest = new PersonTest(); | |
personTest.test="test"; | |
System.out.println(personTest.test); | |
PersonTest personTest1 = new PersonTest(); | |
System.out.println(personTest1.test); | |
} | |
} |
值得注意的是静态方法是属于类的所以并不能使用成员变量和成员方法
# 代码块 & 静态代码块
代码块在创建对象的时候会自动执行
public class PersonTest { | |
String name; | |
int age; | |
String nickname; | |
static String test; | |
{ | |
System.out.println("我是代码块"); | |
} | |
} |
代码块是用 {} 包裹的一块内容
public class testMain { | |
public static void main(String[] args) { | |
PersonTest personTest = new PersonTest(); | |
} | |
} |
静态代码块是有 static 关键字声明的代码块
public class PersonTest { | |
String name; | |
int age; | |
String nickname; | |
static String test; | |
static { | |
System.out.println("我是静态代码块"); | |
} | |
{ | |
System.out.println("我是代码块"); | |
} | |
PersonTest(){ | |
System.out.println("构造函数执行"); | |
} | |
} |
静态代码块,代码块,构造方法,这里看一下他们的执行循序
public class testMain { | |
public static void main(String[] args) { | |
PersonTest.test=null; | |
} | |
} |
实际上 java 是将 class 文件交友 jvm 执行,但是 jvm 不会一开始就加载他,为了资源的合理利用只有在需要的时候才会加载,一般当访问类的静态变量、new 关键字创建实例等等情况下会加载,而金泰方法会在类加载的时候就完成分配,所以静态内容会在对象初始化之前就完成加载
# 访问权限控制
注意到前面的示例代码类、方法和变量前都有 public 关键字,这里就简单记录一下
- private 私有,只有当前类可以访问
- protected 受保护,同一个包下的类可以访问,也可以被子类访问
- public 公共,没有限制
- 【空】默认,同一个包下的类可以访问,不能被子类访问
# 封装 & 继承 & 多态
这三个特性是面向对象的三大特性
# 封装
封装主要是为了使代码透明化,使用者不需要知道具体的实现细节,为了保证变量的安全性,通过外部接口来访问类的成员
public class PersonTest { | |
private String name; | |
private int age; | |
private String nickname; | |
} |
将成员变量都设置为 private 这样想要直接访问就会提示报错
将其封装好
public class PersonTest { | |
private String name; | |
private int age; | |
private String nickname; | |
public PersonTest(String name, int age, String nickname) { | |
this.name = name; | |
this.age = age; | |
this.nickname = nickname; | |
} | |
public String getNickname() { | |
return nickname; | |
} | |
public void setNickname(String nickname) { | |
this.nickname = nickname; | |
} | |
public int getAge() { | |
return age; | |
} | |
public void setAge(int age) { | |
this.age = age; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public String getName() { | |
return name; | |
} | |
} |
再想要使用这个 name 就用这个封装好的 getname 方法
public class testMain { | |
public static void main(String[] args) { | |
PersonTest personTest =new PersonTest("小明",20,"Clown"); | |
System.out.printf(personTest.getName()); | |
} | |
} |
再总结一下,封装就是隐藏具体代码细节
# 继承
前面在说明类的时候使用了人类这个例子,人类还可以细分,比如说学生类,老师类、工人类等等这些,都有一些共同的属性,比如说都有名字,年龄等等,如果我们每个类都定义这些成员变量毫无疑问代码量是非常大的,但是我们可以将这些共同的属性定义到人类中,并且做一个继承,使用关键字 extends
public class PersonTest { | |
public String name; | |
public int age; | |
public String nickname; | |
} |
上面这个类是父类
public class studentTest extends PersonTest{ | |
public String bookname; | |
} |
这个学生类就继承了 PersonTest 类,子类继承后拥有父类的所有共有属性
public class testMain { | |
public static void main(String[] args) { | |
StudentTest studentTest = new StudentTest(); | |
studentTest.name="小王"; | |
} | |
} |
这里虽然子类中没有 name 这个成员变量,但是也是可以正常使用的
值得注意的是,如果父类中构造方法重载为有参函数,在子类中需要在构造方法中调用,这里因为子类在实例化的时候子类和父类的属性都要加载,在没有定义构造函数的情况下,子类实际上是调用了父类的无参构造方法的,如果重载就会覆盖掉无参了,所以需要进行一个指定
public class PersonTest { | |
public String name; | |
public int age; | |
public String nickname; | |
public PersonTest(String name) | |
{ | |
this.name=name; | |
} | |
} |
父类中重载了一个有参的构造方法
public class StudentTest extends PersonTest{ | |
public String bookname; | |
public StudentTest(String name) { | |
super(name); | |
} | |
} |
子类中需要使用 super 来指定这个构造方法。
this 关键字表示的是当前这个对象的属性
# 多态
# 方法の重写
前面有提到重载,这与重写是完全不同的,通过重载可以给与同一个方法名不同的使用种类,而重写是覆盖掉原有的方法
父类 PersonTest
public class PersonTest { | |
public String name; | |
public int age; | |
public String nickname; | |
public PersonTest(String name) | |
{ | |
this.name=name; | |
} | |
public void eatTest(){ | |
System.out.println("干饭人努力干饭"); | |
} | |
} |
在父类中有一个 eatTest 方法
子类 StudentTest
public class StudentTest extends PersonTest{ | |
public String bookname; | |
public StudentTest(String name) { | |
super(name); | |
} | |
@Override | |
public void eatTest() { | |
System.out.println("干饭人"); | |
} | |
} |
在子类中重写了一个 eatTest 方法
重写方法要与原方法传参数量类型相同
每个人的想法不同,不同的人对于同一件事有着不同的行为,先看下面是示例(这里 PersonTest 类没有改变)
StudentTest 类
public class StudentTest extends PersonTest{ | |
public String bookname; | |
public StudentTest(String name) { | |
super(name); | |
} | |
@Override | |
public void eatTest() { | |
System.out.println("去学生餐厅吃饭"); | |
} | |
} |
这里重写了 PersonTest 的 eatTest 方法
TeacherTest 类
public class TeacherTest extends PersonTest{ | |
public TeacherTest(String name) { | |
super(name); | |
} | |
@Override | |
public void eatTest() { | |
System.out.println("去教室餐厅吃饭"); | |
} | |
} |
这里重写了 PersonTest 的 eatTest 方法
public class testMain { | |
public static void main(String[] args) { | |
StudentTest studentTest = new StudentTest("小王"); | |
studentTest.eatTest(); | |
TeacherTest teacherTest = new TeacherTest("王老师"); | |
teacherTest.eatTest(); | |
} | |
} |
这里小王和王老师调用 eatTest 方法
这就是多态的一种表现,同一个行为对不同的对象有着不同的表现形式
使用 super 关键字可以调用父类的实现,使用 final 关键字修饰的方法和属性不能再继承
# 抽象类
抽象类中可以有抽象方法,抽象方法简单来说,只有方法的定义,没有具体实现的细节或是细节很少,都交由子类实现具体细节
public abstract class PersonTest { | |
public String name; | |
public int age; | |
public String nickname; | |
public PersonTest(String name) | |
{ | |
this.name=name; | |
} | |
public abstract void eatTest(); | |
} |
像这个示例中使用了 abstract 关键字来修饰,表示这个类是一个抽象类,下面定义了一个抽象方法 eatTest
继承与其的子类必须实现其抽象方法(如果子类也是抽象类的话可以不实现)
# 面向对象(二)
# 包装类
前面简单的记录了一下 java 面向对象的一些基础的东西,我们一直在对象,属性云云,但是 java 中的基本数据类型不是面向对象的,java 提供了一个基本包装类,使得 java 基本数据类型能够支持独享操作
下表中的基本类型的包装类对应表
byte | Byte |
---|---|
boolean | Boolean |
short | Short |
char | Character |
int | Integer |
long | Long |
float | Float |
double | Double |
左边是基本数据类型,右边是对应的包装类
public class PackageClassTest { | |
public static void main(String[] args) { | |
Byte b = new Byte("4"); | |
Integer i = new Integer("1"); | |
Boolean B = true; | |
} | |
} |
上面的代码块中给了一个使用示例,这是简单的定义方式,包装类是支持装箱和拆箱的
public class PackageClassTest { | |
public static void main(String[] args) { | |
Integer t = Integer.valueOf(10); | |
int a = t.intValue(); | |
} | |
} |
装箱和拆箱可以让包装类参与到基本类型的运算中
下面来说两个特殊的包装类
我们知道基础数据类型的最大数值是 2147483647,已经是一个很大的数值了,但是在商业用途,或者是对于一些计数场合中,这个大小的数值还是不够用的,java 中有一个计算超大数值的 BIgInterger
public class PackageClassTest { | |
public static void main(String[] args) { | |
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE); | |
BigInteger b = i.pow(10000); | |
System.out.println(b); | |
} | |
} |
第二个特殊的包装类也是关于数值计算的,浮点数的精度有限,BigDecimal 可以实现小数的精确计算
public class PackageClassTest { | |
public static void main(String[] args) { | |
double d =10; | |
System.out.println(d/3); | |
BigDecimal dou = BigDecimal.valueOf(10); | |
System.out.println(dou.divide(BigDecimal.valueOf(3),100, RoundingMode.FLOOR)); | |
} | |
} |
# 可变长参数
一个小点,浅记一下
public class PackageClassTest { | |
public static void main(String[] args) { | |
test("a","b","c"); | |
} | |
public static void test(String... str){ | |
for (String s : str) { | |
System.out.println(s); | |
} | |
} | |
} |
看到这里在 test 方法中,String... 表示可以接受任意多个参数
如果还有其他类型的话,可变长参数在一个方法中只能有一个,并且要放到最后
# 内部类
顾名思义,定义在类内部的类
public class InnerClass { | |
public class Test{ | |
} | |
} |
上面就是一个简单的内部类,InnerClass 中又定义了一个 Test 类
class main { | |
public static void main(String[] args) { | |
InnerClass innerClass = new InnerClass(); | |
InnerClass.Test test = innerClass.new Test(); | |
} | |
} |
用这种方法来实例化内部类,内部类也是可以使用关键词 static 来修饰的,区别是什么呢?
# static 内部类
内部类是属于对象的,而静态的内部类是属于类的,可以直接创建使用
public class InnerClass1 { | |
public class Test1{ | |
public void test1() { | |
System.out.println("内部类的test方法被调用了"); | |
} | |
} | |
} | |
class InnerClass2{ | |
public static class Test2{ | |
public void test2(){ | |
System.out.println("静态内部类的test方法被调用了"); | |
} | |
} | |
} | |
class main { | |
public static void main(String[] args) { | |
InnerClass1 innerClass = new InnerClass1(); | |
InnerClass1.Test1 test1 = innerClass.new Test1(); | |
test1.test1(); | |
InnerClass2.Test2 test2 = new InnerClass2.Test2(); | |
test2.test2(); | |
} | |
} |
这里很明显了吧,可以看到这里 new 实例的方式不同,静态的方法是属于类的所以可以直接实例化
# 匿名内部类
上面的内部类是在类中创建,其实这里把这个类大致看成 c 语言中的结构体就好,方法中同样也是可以创建一个类的,这种类叫做局部内部类,只在当前方法中能够使用
局部内部类的使用频度并不是很高,反而是与它有着很多共同点的匿名内部类使用的频率很高,在前面已经记录过抽象类和接口的相关知识,我们要知道抽象类和接口都是不能直接里用 new 去创建一个对象的,但是我们可以使用匿名内部类了创建
public abstract class PersonTest { | |
public abstract void test(); | |
} |
这里创建一个抽象类
public class MainTest { | |
public static void main(String[] args) { | |
PersonTest personTest = new PersonTest() { | |
@Override | |
public void test() { | |
System.out.println("匿名"); | |
} | |
}; | |
personTest.test(); | |
} | |
} |
创建了一个匿名内部类来实现抽象方法
# Lambda 表达式
对于抽象类和接口我们可以通过匿名内部类来创建一个对象,如果接口中只有一个 需要实现的抽象方法,那么就可以将其写成 Lqmbda 表达式,可以狭义的理解为匿名函数的一种简写
public abstract interface PersonTest { | |
public abstract void test(); | |
} |
简写为 Lambda 表达式
public class MainTest { | |
public static void main(String[] args) { | |
PersonTest personTest = () -> System.out.println("aaaa"); | |
personTest.test(); | |
} |
:::
值得注意的是这里是对应接口
:::