[TOC]
记录一下学习java
时觉得重点或一时没记住的知识点,方便日后复习,持续更新,有东西就加
java基础
认识java
Java介于编译型语言和解释型语言之间。编译型语言如C、C++,代码是直接编译成机器码执行,但是不同的平台(x86、ARM等)CPU的指令集不同,因此,需要编译出每一种平台的对应机器码。
解释型语言如Python、Ruby没有这个问题,可以由解释器直接加载源码然后运行,代价是运行效率太低。
而Java是将代码编译成一种“字节码”,它类似于抽象的CPU指令,然后,针对不同平台编写虚拟机,不同平台的虚拟机负责加载字节码并执行,这样就实现了“一次编写,到处运行”的效果。当然,这是针对Java开发者而言。
对于虚拟机,需要为每个平台分别开发。为了保证不同平台、不同公司开发的虚拟机都能正确执行Java字节码,SUN公司制定了一系列的Java虚拟机规范。从实践的角度看,JVM的兼容性做得非常好,低版本的Java字节码完全可以正常运行在高版本的JVM上。
- Java SE(Standard Edition): 标准版,包含标准的JVM和标准库
- Java EE(Enterprise Edition): 企业版,在Java SE的基础上加上了大量的API和库,以便方便开发Web应用、数据库、消息服务等
- Java ME(Micro Edition): 针对嵌入式设备的“瘦身版”,Java SE的标准库无法在Java ME上使用,Java ME的虚拟机也是“瘦身版”
1 | ┌───────────────────────────┐ |
- JRE(Java Runtime Environment): 运行Java字节码的虚拟机
- JDK(Java Development Kit)
如果只有Java源码,要编译成Java字节码,就需要JDK,JDK除了包含JRE,还提供了编译器、调试器等开发工具。
1 | ┌─ ┌──────────────────────────────────┐ |
- JSR规范:Java Specification Request
- JCP组织:Java Community Process
为了保证Java语言的规范性,SUN公司搞了一个JSR规范,凡是想给Java平台加一个功能,比如说访问数据库的功能,大家要先创建一个JSR规范,定义好接口,这样,各个数据库厂商都按照规范写出Java驱动程序,开发者就不用担心自己写的数据库代码在MySQL上能跑,却不能跑在PostgreSQL上。
所以JSR是一系列的规范,从JVM的内存模型到Web程序接口,全部都标准化了。而负责审核JSR的组织就是JCP。
一个JSR规范发布时,为了让大家有个参考,还要同时发布一个“参考实现”,以及一个“兼容性测试套件”:
- RI:Reference Implementation
- TCK:Technology Compatibility Kit
比如有人提议要搞一个基于Java开发的消息服务器,这个提议很好啊,但是光有提议还不行,得贴出真正能跑的代码,这就是RI。
如果有其他人也想开发这样一个消息服务器,如何保证这些消息服务器对开发者来说接口、功能都是相同的?所以还得提供TCK。
通常来说,RI只是一个“能跑”的正确的代码,它不追求速度,所以,如果真正要选择一个Java的消息服务器,一般是没人用RI的,大家都会选择一个有竞争力的商用或开源产品。
运行一个Demo,文件名为Hello.java
,代码如下
1 | public class Hello { |
javac
命令将Hello.java
文件编译成字节码文件Hello.class
,然后用java
命令执行这个字节码文件
1 | D:\Desktop\Java_project>javac Hello.java |
注意:给虚拟机传递的参数Hello是我们定义的类名,虚拟机自动查找对应的class文件并执行,
Java 11之后,可以通过java Hello.java
可以直接运行一个单文件源码,需要注意的是,在实际项目中,单个不依赖第三方库的Java源码是非常罕见的。
所以,绝大多数情况下,我们无法直接运行一个Java源码文件,原因是它需要依赖其他的库。
- 一个Java源码只能定义一个public类型的class,并且class名称和文件名要完全一致;
- 使用
javac
可以将.java源码编译成.class字节码; - 使用
java
可以运行一个已编译的Java程序,参数是类名。
基础语法
Java是面向对象的语言,一个程序的基本单位就是class,类名要求:必须以英文字母开头(大写),后接字母,数字和下划线的组合
Java入口程序规定的方法必须是静态方法,方法名必须为main,括号内的参数必须是String数组。
方法名也有命名规则,命名和class一样,但是首字母小写,在方法内部,语句才是真正的执行代码。Java的每一行语句必须以分号结束。
1 | public class Hello { |
注释有三种:
1 | // 单行注释 |
第三种以/**
开头,以*/
结束,如果有多行,每行通常以星号开头,需要写在类和方法的定义处,可以用于自动创建文档。
数据类型
基本类型
基本类型如下:
- 整数类型:byte,short,int,long
- 浮点数类型:float,double
- 字符类型:char
- 布尔类型:boolean
计算机内存的最小存储单元是字节(byte),一个字节就是一个8位二进制数,即8个bit,不同数据类型所占字节数:
- byte 1字节 -128 ~ 127
- short 2字节 -32768 ~ 32767
- char 2字节
- int 4字节 -2147483648 ~ 2147483647
- float 4字节
- long 8字节 -9223372036854775808 ~ 9223372036854775807
- double 8字节
1 | public class Main { |
整数运算:
- 整数的除法对于除数为0时运行时将报错,但编译不会报错
- 整数除法不能整除会舍弃小数
- 溢出不会出错,却会得到一个奇怪的结果
i++ ++i << >> >>>无符号右移 & | ~非 ^异或
- 类型不统一时,会自动转换:在运算过程中,如果参与运算的两个数类型不一致,那么计算结果为较大类型的整型
- 支持强制转换,如
short s = (short) i
浮点数运算:
- 只能进行加减乘除这些数值计算,不能做位运算和移位运算
- 浮点数常常无法精确表示,如0.1,二进制是无限循环小数,但0.5又可以精确地表示,因此,浮点数比较大小有时是不准确的(见下方代码)
- 其他与整形运算相似
1 | public class Main { |
如果非要比较,可以设置一个阈值,看二者相差是否在这之间:
1 | // 比较x和y是否相等,先计算其差的绝对值: |
布尔运算:
- 短路运算:如果一个布尔运算的表达式能提前确定结果,则后续的计算不再执行,直接返回结果
- 支持三元运算符:
b ? x : y
引用类型
除了基本类型,剩下的都是引用类型,最常用的是String
,引用类型的变量类似于C语言的指针,它内部存储一个“地址”,指向某个对象在内存的位置
String
,用双引号”…”表示字符串。一个字符串可以存储0个到任意个字符,转义:
- " 表示字符”
- ' 表示字符’
- \ 表示字符\
- \n 表示换行符
- \r 表示回车符
- \t 表示Tab
- \u#### 表示一个Unicode编码的字符
字符串拼接可直接使用+
。如果用+连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接。
从Java 13开始,字符串可以用”””…”””表示多行字符串(Text Blocks)了:
1 | public class Main { |
引用类型的变量可以指向一个空值null,它表示不存在,即该变量不指向任何对象。例如:
1 | String s1 = null; // s1是null |
常量:定义变量的时候,如果加上final修饰符,这个变量就变成了常量,常量在定义时进行初始化后就不可再次赋值,再次赋值会导致编译错误。根据习惯,常量名通常全部大写。
1 | final double PI = 3.14; // PI是一个常量 |
有些时候,类型的名字太长,写起来比较麻烦。例如:
1 | StringBuilder sb = new StringBuilder(); |
这个时候,如果想省略变量类型,可以使用var关键字,编译器会根据赋值语句自动推断出变量sb的类型是StringBuilder
1 | var sb = new StringBuilder(); |
多行语句用{ }括起来,括号内部是自身的范围,在语句块中定义的变量,它有一个作用域,就是从定义处开始,到语句块结束。超出了作用域引用这些变量,编译器会报错。
1 | public class Main { |
数组:
1 | public class Main { |
- 数组所有元素初始化为默认值,整型都是0,浮点型是0.0,布尔型是false
- 数组一旦创建后,大小就不可改变
流程控制
// todo 输入输出
输入输出:这块之后系统整理下
if判断语句:
1 | if (条件) { |
- 引用类型判断内容相等要使用equals(),注意避免NullPointerException【重点】
- 浮点数判断相等不能直接用==运算符
- 不推荐省略花括号{} 一条语句时大括号可以省,但不推荐
1 | public class Main { |
switch-case语句
- switch的计算结果必须是整型、字符串或枚举类型
- case语句具有穿透性,要带break,可以打开fall-through警告
- 不要漏掉default,可以打开missing default警告
- Java 14开始,switch语句正式升级为表达式,使用
->
格式时,不再需要break,允许携带返回值,通过yield提前中断(类似于return)
1 | public class Main { |
循环语句
1 | while (条件表达式) { |
数组操作
1 | import java.util.Arrays; |
面向对象
Java是一种面向对象的编程语言。面向对象编程,英文是Object-Oriented Programming,简称OOP
- 一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中
- class和instance是“模版”和“实例”的关系,定义class就是定义了一种数据类型,对应的instance是这种数据类型的实例
- class定义的field,在每个instance都会拥有各自的field,且互不干扰
- 通过new操作符创建新的instance,然后用变量指向它,即可通过变量来引用这个instance
- 指向instance的变量都是引用变量
方法
1 | 修饰符 方法返回类型 方法名(方法参数列表) { |
- 方法内部可以使用this访问当前实例
- 方法支持可变参数,如
public void setNames(String... names) {}
- 可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null
setNames
此时的参数names是String[]
类型
构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。
1 | public class Main { |
- 实例在创建时通过new操作符会调用其对应的构造方法,构造方法用于初始化实例
- 没有定义构造方法时,编译器会自动创建一个默认的无参数构造方法
- 可以定义多个构造方法,编译器根据参数自动判断
- 可以在一个构造方法内部调用另一个构造方法,便于代码复用
方法重载(overload):如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。
注意:方法重载的返回值类型通常都是相同的。
方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。举个例子,String类提供了多个重载方法indexOf(),可以查找子串:
- int indexOf(int ch):根据字符的Unicode码查找
- int indexOf(String str):根据字符串查找
- int indexOf(int ch, int fromIndex):根据字符查找,但指定起始位置
- int indexOf(String str, int fromIndex)根据字符串查找,但指定起始位置
类
继承
- 继承是面向对象编程的一种强大的代码复用方式
- Java只允许单继承,所有类最终的根类是Object
- protected允许子类访问父类的字段和方法
- 子类的构造方法可以通过super()调用父类的构造方法
- 可以安全地向上转型为更抽象的类型
- 可以强制向下转型,最好借助instanceof判断
- Java 14后instanceof判断完可直接赋值
- 子类和父类的关系是is,has关系不能用继承,可考虑组合
使用extends关键字来实现继承。超类(super class),父类(parent class),基类(base class) | 子类(subclass),扩展类(extended class)
注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!
子类无法访问父类的private字段或者private方法,为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问
使用super可以指向父类(对应this),子类的构造方法必须调用父类的构造方法,方式为super()
并携带上对应参数
正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。
- 向上转型:把一个子类型安全地变为更加抽象的父类型【没问题】
- 向下转型:把一个父类类型强制转型为子类类型【看原本是什么,如果原本就能转那没问题,否则会报错
ClassCastException
】
向下转型举例:
1 | Person p1 = new Student(); // upcasting, ok |
Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException。
为了防止向下转型发生问题,一般先通过instanceof
方法判断是否为该实例
1 | Person p = new Person(); |
从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:
1 | Object obj = "hello"; |
可以改写如下:
1 | public class Main { |
多态
- 子类可以覆写父类的方法(Override),覆写在子类中改变了父类方法的行为
- Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态
- final修饰符有多种作用
- final修饰的方法可以阻止被覆写
- final修饰的class可以阻止被继承
- final修饰的field必须在创建对象时初始化,随后不可修改
重载overload方法签名不同,覆写(重写)override方法签名不同相同
加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。
多态的特性就是,运行期才能动态决定调用的子类方法。多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
1 | public class Main { |
抽象类
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法:
1 | class Person { |
把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。
必须把Person类本身也声明为abstract,才能正确编译它:
1 | abstract class Person { |
我们无法实例化一个抽象类:Person p = new Person(); // 编译错误
。抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。
当我们定义了抽象类Person,以及具体的Student、Teacher子类的时候,我们可以通过抽象类Person类型去引用具体的子类的实例,这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person类型变量的具体子类型:
1 | Person s = new Student(); |
这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
- 上层代码只定义规范(例如:abstract class Person)
- 不需要子类就可以实现业务逻辑(正常编译)
- 具体的业务逻辑由不同的子类实现,调用者并不关心
接口
如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口:interface
所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。
1 | interface Person { |
当一个具体的class去实现一个interface时,需要使用implements关键字。举个例子:
1 | class Student implements Person { |
在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
:class Student implements Person, Hello {...}
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
字段 | 可以定义实例字段 | 不能定义实例字段,可定义静态字段且为final 类型 |
抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
在接口中,可以定义default方法。实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。
1 | public class Main { |