# java 运行机制
- .java 文件是源代码,.class 文件是字节码,javac.exe 编译源代码为字节码
- 每有一个类,编译后都会生成一个.class 文件
JDK JRE JVM
JDK = JRE + Java 开发工具
JRE = JVM + 核心类库
JVM:Java virtual machine
# 注意事项:
- 源文件的组成部分是类(class)
- java 应用程序入口是 main () 方法,固定书写格式:public static void main (String [] args) {}
- 一个源文件最多只能有一个 public 类,其他不限
- 如果源文件包含一个 public 类,那文件名必须按该类名命名
- main 方法可以写在非 public 类,然后指定运行非 public 类,这样入口方法就是非 public 类的 main 方法
# 转义字符
\t:制表位,对齐,固定宽度
\r:一个回车,对齐到第一个字并逐个替换,比如 System.out.println (“我爱你 \r 他”),会输出他爱你
如果是 System.out.println (“我爱你 \r\n 他”) 就会输出两行
我爱你
他
# 文档注释
Javadoc 写法和标签总结_javadoc 标签大全 - CSDN 博客
在方法的上面输入
1 | /** |
然后回车,就会自动生成一个 javadoc 格式的文档,然后就可以填内容了
和其他注释最主要的区别就是 javadoc 的注释可以在我们鼠标放到方法名 / 类名 / 变量名上面的时候,直接看到其内容,而双斜杠的注释不行
所以按照一般来说,我们会在类 / 变量 / 方法的注释中使用 javadoc 的形式,而在某段代码的注释使用双斜杠形式
# 用法
javadoc -d 文件夹名 -xx -yy Demo3.java
假设在 d 盘且文件名为 tch,所以下面这个就是
javadoc -d d:\ -author -version tch.java
1 | /** |
# 常用 javadoc 标签
标签 | 描述 | 标签类型 |
---|---|---|
@author | 作者标识 | 包、 类、接口 |
@deprecated | 标识当前 API 已经过期,仅为了保证兼容性依然存在,以此告之开发者不应再用这个 API | 包、类、接口、值域、构造函数、 方法 |
指明当前文档根目录的路径 | ||
@exception | 标志一个类抛出的异常 | 构造函数、 方法 |
从直接父类继承的注释 | ||
链接到某个特定的成员对应的文档中 | 包、类、接口、值域、构造函数、 方法 | |
插入一个到另一个主题的链接,但是该链接显示纯文本字体 | 包、类、接口、值域、构造函数、 方法 | |
@param | 方法的入参名及描述信息,如入参有特别要求,可在此注释 | 构造函数、方法 |
@return | 对函数返回值的注释 | 方法 |
@see | 引用,查看相关内容,如类、方法、变量等 | 包、类、接口、值域、构造函数、 方法 |
@serial | 说明一个序列化属性 | |
@serialData | 说明通过 writeObject () 和 writeExternal () 方法写的数据 | |
@serialField | 说明一个 ObjectStreamField 组件 | @ |
@since | 描述文本,API 在什么程序的什么版本后开发支持 | 包、类、接口、值域、构造函数、 方法 |
@throws | 构造函数或方法所会抛出的异常 | 构造函数、 方法 |
显示常量的值,该常量必须是 static 属性 | 静态值域 | |
@version | 版本号 | 包、 类、接口 |
对两种标签格式的说明: |
- @tag 格式的标签(不被 { } 包围的标签)为块标签,只能在主要描述(类注释中对该类的详细说明为主要描述)后面的标签部分(如果块标签放在主要描述的前面,则生成 API 帮助文档时会检测不到主要描述)。
- {@tag} 格式的标签(由 { } 包围的标签)为内联标签,可以放在主要描述中的任何位置或块标签的注释中
# 数据类型转换细节
# 自动转换
- (byte short)和 char 之间不会相互自动转换
- byte short char 他们三者可以计算,在计算时首先转换为 int 类型
- 哪怕是相同的类型,比如 byte 和 byte 计算,他也会先转成 int 再计算,结果类型也就是 int,另外两个也是的
- 布尔类型不参与类型自动转换
# 强制转换
1 | double a = 123.5; |
# 位移运算
- 算术右移 >>:低位溢出,符号位不变,并用符号位补溢出的高位,可以理解为右移一位就除以 2
- 算术左移 <<:符号位不变,低位补 0,可以理解为左移一位就乘以 2
- '>>>' 逻辑右移也叫无符号右移,运算规则是:低位溢出,高位补 0
# 数组
-
不知道值的时候比如需要输入就用动态,已知数组的值比如用来加密的表就用静态
-
数组如果未赋值,每个类型都有其对应的默认值:数字类型全是 0 或 0.0(浮点),char 为 \u000,boolean 为 false,String 为 null
-
数组的赋值为地址复制,如果只想值复制的话,可以先分配空间后再用遍历赋值
1
2
3
4
5int arr1[]={1,2,3};
int arr2 = new int[arr1.length];
for(int i=0;i<arr1.length;i++) {
arr2[i] = arr1[i];
};
# 动态初始化
int [] arr= new arr [10]; 或者 int arr [] = new arr [10];
也可以先声明再分配空间:
int arr[];
arr = new int[10];
# 静态初始化
int tch[] = {1,2,3,4,5};
# 扩容
用新数组扩容并转移地址
1 | int arr[] = {1,2,3}; |
# 二维数组
-
java 中二维数组的列数可以不确定,第一个一维可以有五个元素,第二个一维可以只有 3 个元素
1
2
3
4int arr[][] = new int[3][];
for(int i=0;i<arr.length;i++) {
arr[i] = new int[i+1];//在这里给每个一维数组分配长度
}
1 | int arr[][] = { {1,2,3}, {4,5,6} };//静态 |
# 类与对象
# 创建规则
1 | class Cat { |
- 对象的复制是指向地址的
# 类和对象的内存分配机制
- 栈:一般存放基本数据类型
- 堆:存放对象(Cat cat,数组等)
- 方法区:常量池(常量,比如字符串),类加载信息
# 创建对象的流程简单分析
1 | Person p = new Person(); |
- 先加载 Person 类信息(属性和方法信息,只加载一次)
- 在堆中分配空间,进行默认初始化(默认值)
- 把地址赋给 p,让 p 指向对象
- 进行指定的初始化,比如上面代码的赋值
# 方法
# 如何创建和引用
1 | public class tch{ |
# 方法调用过程
- 程序执行到方法时,会开辟一个独立空间(栈空间)
- 当方法执行完毕,或者执行到 return 语句时,就会返回
- 返回到调用方法的地方
- 返回后继续执行方法后面的代码
- 当 main 方法 (栈) 执行完毕后整个程序退出
# 一些细节
- 有多个返回值的时候可以用数组
- void 类型不可以 return 值,但可以只写 return
- 同类中的方法可以直接调用,不用创建对象
# 传参机制
- 基本数据类型的传参就是拷贝
- 引用数据类型的传参是指向地址
# 递归
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量时独立的,不会相互影响
- 如果方法中使用的是引用类型变量,就会共享该引用类型的数据
递归主要是对于规律的应用吧,只要微操作和超级操作是对的,就不要去想里面的过程是怎么样,两个操作都是正确的,其实也就互相证明了这个程序是对的,要相信它可以实现我们的要求
# 方法重载
java 中允许同一个类中有多个同名方法的存在,但形参列表要不一样。重载减轻了起名和记名的麻烦
1 | //比如最基础的 |
- 方法名要一致
- 形参列表必须不同,类型、个数、顺序等
- 返回类型:无要求
# 可变参数
java 允许将同一个类中,多个同名同功能但参数个数不同的方法,封装成一个方法。可通过可变参数实现
# 基本语法
1 | 访问修饰符 返回类型 方法名(数据类型...形参名) { |
# 细节
- 可变参数的个数可以是 0 也可以是任意多个
- 可变参数的本质就是数组
- 可变参数可以和普通类型的参数一起放在形参列表,但是必须保证可变参数在最后,且一个形参列表中只能有一个可变参数
1 | public void f1(int a,double b,char... c) { |
# 作用域
- java 中主要的变量就是属性(成员变量)和局部变量
- 局部变量一般是指在成员方法中定义的变量
1 | class cat { |
- java 中作用域的分类:
全局变量:也就是属性,作用域为整个类体
局部变量:也就是除了属性之外的其他变量,作用域为定义它的代码块中
-
全局变量可以不赋值,直接使用,因为有默认值,局部变量必须赋值后才能使用,因为没有默认值
-
局部变量和属性(全局变量)可以重名,访问时遵循就近原则
1 | class cat { |
- 可以跨类调用全局变量
- 全局变量可以加修饰符,局部不可以
# 构造器 / 方法
- 构造器的修饰符可以默认,也可以是 public,protected,private
- 构造器没有返回值
- 方法名和类名必须一样
- 参数列表和成员方法一样的规则
- 构造器的调用系统完成,在创建对象时,系统会自动调用该类的构造器完成对对象的初始化
1 | public class constructor { |
- 构造器也可以重载,因为构造器本身也是方法
1 | public class constructor { |
- 程序员如果没定义构造器,系统会自动生成一个默认无参构造器
- 有参构造器在 new 新对象的时候要提供参数,无参构造器不需要,相当于给了多种 new 新对象的方式
1 | class Person { |
- 一旦定义了自己的构造器,默认的构造器就被覆盖了,不能再使用默认无参构造器,除非显式地定义一下
1 | class Person { |
# this 关键字
在这里定义构造器的时候,如果可以把形参定义为 String name, int age 看上去就会更简介,但是如果这样定义就会导致 name 的值为空,这里可以用 this 来解决
1 | class Dog { |
java 虚拟机会给每个对象分配 this,代表当前对象,哪个对象调用 this 就指的是哪个对象的地址
1 | class Dog { |
- this 不能在类定义的外部使用,只能在类定义的方法中使用
- 访问成员方法的语法:this. 方法名 (参数列表)
1 | class T { |
- 访问构造器语法:this (参数列表);只能在构造器中使用(只能在构造器中访问另一个构造器)
1 | public T { |
# 包
包实际上就是创建不同的文件夹来保存类文件
# 作用:
- 区分相同名字的类
- 当类很多时,可以很好的管理类
- 控制访问范围
# 命名规范
1 | //一般的命名规范 |
# 常用的包
1 | java.lang.* //lang包时基本包,默认引入 |
# 细节
- package 的作用是声明当前类所在的包,需要放在类的最上面,一个类中最多只有一句 package
- import 指令位置放在 package 的下面,在类定义前面,可以有多句且没有顺序要求
# 访问修饰符
- 公开级别:用 public 修饰,对外公开
- 受保护级别:protected,对子类和同一个包中的类公开
- 默认级别:没有修饰符号,向同一个包的类公开
- 私有级别:private,只有类本身可以访问,不对外公开
# 封装 encapsulation
封装就是把抽象出的数据 [属性] 和对数据的操作 [方法] 封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作 [方法],才能对数据进行操作
# 封装的好处
- 隐藏实现细节:方法 (连接数据库)<–调用 (传入参数)
- 可与对数据进行验证,保证安全合理
# 封装的实现步骤
-
将属性进行私有化【不能直接修改属性】
-
提供一个公共的 (public) set 方法,用于对属性判断并赋值
1 | public void setXxx(类型 参数名){//Xxx表示某个属性 |
- 提供一个公共的 (public) get 方法,用于获取属性的值
1 | public 数据类型 getXxx(){//权限判断,Xxx某个属性 |
# Getter 和 Setter
Getter 和 Setter 方法可以直接在别的类中访问或修改私有属性,它们提供了封装性,检验和操作的功能,通过规范的命名和方法签名,可以提高代码的可读性和维护性
快捷键:Alt + insert(笔记本加 Fn)
# 作用
-
Getter 和 Setter 方法的使用可以有效地控制对对象属性的访问和修改,提供了封装性和安全性
-
通过 Getter 方法,其他类可以获取属性的值而不需要直接访问属性
-
通过 Setter 方法,其他类可以修改属性的值而不需要直接修改属性
-
封装性:通过 Getter 和 Setter 方法,可以对属性的访问和修改进行控制
-
验证和操作:可以在 Getter 和 Setter 方法中实现对属性的验证和操作,例如范围检查、格式转换等,确保数据的合法性
-
可读性和可维护性:通过 Getter 和 Setter 方法可以提供更具描述性和可读性的代码,使代码更易于理解和维护
# 继承
在一些问题中可能会出现,很多类的属性和方法都一样,这就造成了代码复用的现象,代码冗余度很高,继承就可以解决代码复用性的问题。
当多个类存在相同属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过 extends 来声明继承父类即可。
# 语法
1 | class 子类 extends 父类 { |
# 细节
- 子类继承了所有的属性和方法,但是私有属性和方法不能在子类直接访问,要通过公共的方法去访问
- 子类必须调用父类的构造器,完成对父类的初始化
- 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用 super 去指定使用父类的哪个构造器完成对父类的初始化工作,否则编译不会通过
1 | //比如现在的父类就没有无参构造器,但是有形参为String,int的构造器,那么可以这样使用super(); |
- 如果希望指定去调用父类的某个构造器,则显示的调用一下
- super 在使用时,需要放在构造器第一行
- super () 和 this () 都只能放在构造器的第一行,因此这两个方法不能共存在一个构造器
- java 所有类都是 Object 类的子类,Object 类是所有类的基类
- 父类构造器的调用不限于直接父类,将一直往上追溯,直到 Object 类 (顶级父类)
- 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制。
如果要让 A 继承 B 和 C,得先 A 继承 B,B 继承 C
- 不能滥用继承,子类和父类之间必须满足 is-a 的逻辑关系
1 | Cat extends Animal//合理 |
# super 关键字
用 super 调用父类的构造器,可使分工明确,父类属性由父类初始化,子类的属性由子类初始化
# 语法
- 访问父类的属性,但不能访问父类的 private 属性 super. 属性名
1 | System.out.println(super.n1+super.n2); |
- 访问父类的方法,不能访问父类的 private 方法 super. 方法名 ()
1 | super.test(); |
- 访问父类的构造器 super (参数列表);只能放在构造器的第一句
1 | super("jack"); |
# 细节
- 当子类中有和父类中的成员重名时,为了访问父类的成员,必须通过 super,如果没有重名,使用 super、this、直接访问时一样的效果
- super 的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用 super 去访问爷爷类的成员;如果多个基类中都有同名的成员,使用 super 访问遵循就近原则。
# 方法重写 / 覆盖 (override)
方法覆盖就是子类有一个方法和父类的某个方法的名称、返回类型、参数一样,那么我们就说子类的这个方法覆盖了父类的那个方法 +
** 作用:** 可以达到不更改父类的方法便创建了子类的一个方法。使程序更加的安全
# 注意事项和使用细节
- 子类的方法的参数,方法名称,要和父类方法的参数名称完全一样
- 子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类,比如父类返回类型是 object,子类返回类型是 String
- 子类方法不能缩小父类方法的访问权限
# 多态
问题引出:一个主人有几个宠物,比如小狗和猫,小狗吃骨头,猫吃鱼,可能还会吃别的东西,那么在这种情况下,我们要建立一个小狗的类、小猫的类,还有食物的类,而食物又分为骨头,鱼,后面可能还会有别的,那么这些食物就是 food 的子类。在这种情况下,我每多一个宠物,我就要在主方法中多写一个方法来调用他们,这样会随着项目的扩展导致方法数量太多,不利于管理和维护
而在多态的情况下,父类对象可以指向子类对象,那么只要是这个父类的子类,都可以直接传入同一个方法中来调用,根据子类的不同而输出不同的值
# 方法的多态
方法的重写和重载就体现了多态
# 对象的多态
- 一个对象的编译类型和运行类型可以不一致
- 编译类型在定义对象时就确定了,不能改变
- 运行类型是可以变化的
- 编译类型看定义时 “=” 号的左边,运行类型看 “=” 号的右边
1 | Animal animal = new Dog();//animal编译类型是Animal,运行类型是Dog |
# 注意事项
- 多态的前提是两个对象(类)存在继承关系
- 属性没有重写之说,属性的值看编译类型。(如果转型的类中有和父类一样的属性,那么就看编译的是哪个类,值就是哪个类的值)
- instanceof 比较操作符,用于判断对象的运行类型是否为 XX 类型或 XX 类型的(前面这个是否为后面这个的)子类型
1 | //编译类型运行类型都为BB,但BB是AA的子类,所以返回结果都为ture |
# 多态向上转型
本质:父类的引用指向了子类的对象(也就是向上转型)。只要是继承关系,父类都可以指向下面的任意子类
特点:编译类型看左边,运行类型看右边
语法:父类类型 应用名 = new 子类类型 ();
1 | Animal animal = new Cat(); |
- 可以调用父类中的所有成员(需遵守访问权限,比如私有就无法调用)
- 不能调用子类中的特有成员(父类中没有的,就不能调用)
- 最终的运行效果看子类的具体实现
# 向下转型
为了代码维护的便利,我们利用多态的性质进行了一个向上转型,但当我们在向上转型之后,又需要用到子类中的成员,但此时无法引用,这还是就可以向下转型进行一个强转,强行把指向父类又改成指向子类,这样就可以使用子类的成员了
语法:子类类型 引用名 = (子类类型)父类引用;
1 | Cat cat = (Cat) animal; |
- 只能强转父类的引用,不能强转父类的对象
- 要求父类引用必须指向的是当前目标类型的对象(就是如果要向下转型,你转型的目标原本就是这个类型的,只不过之前向上转型了,现在相当于是复原回它原本的类型)
- 可以调用子类类型中所有的成员
# java 动态绑定机制
- 当调用对象方法的时候,该方法回合该对象的内存地址 (也就是运行类型) 绑定
- 当调用对象属性时,没有动态绑定机制,哪里声明就用哪里的
# 多态数组
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型
1 | //Student和Teacher都是Person的子类,数组为Person类,里面的数据可以存放子类 |
# 多态参数
方法定义的形参类型为父类类型,实参类型允许为子类类型
1 | Student tch = new Student("tch", 21); |
# Object 类
# ==
- == 既可以判断基本类型,又可以判断引用类型
- == 如果判断基本类型,判断的是值是否相等
- == 如果判断引用类型,判断的是地址是否相等,即判定是不是同一个对象
1 | A a = new A(); |
# equals()
equals 是 Object 类中的方法,只能判断引用类型,默认判断的是地址是否相等,子类中往往要重写该方法,用于判断内容是否相等,比如 Integer,String,这两个之所以可以直接比较值,是因为 integer 类和 String 类中已经重写了 equals 方法
如果没有重写 equals 方法,那么就是比较地址,如果重写了,就是比较重写的值,根据这个我们可以自己重写我们所需要的 equals 方法
1 | class Person { |
# hashCode 方法
返回该对象的哈希码值,支持此方法是为了提高哈希表的性能。
- 提高具有哈希结构的容器的效率
- 两个引用,如果一个指向的是同一个对象,哈希值一样
- 如果指向的是不同的对象,哈希值不一样
- 哈希值主要根据地址号计算,不能完全将哈希值等价于地址
- 后面在集合中,hashCode 如果需要的话,也可以重写
# toString 方法
- 默认返回:全类名 (包名 + 类名)+@+ 哈希值的 16 进制,子类往往重写 toString 方法,用于返回对象的属性信息
- 重写 toString 方法,打印对象或拼接对象时,都会自动调用该对象的 toString 形式
- 当直接输出一个对象时,toString 方法会被默认的调用
1 | //重写toString方法输出对象的属性,使用快捷键alt+insert -> toStirng |
# finalize 方法
这个实际开发中几乎不会用,应付面试
- 当对象被回收时,系统自动调用该对象的 finalize 方法,释放资源,子类可重写该方法
- 什么时候被回收:当某个对象没有任何引用时,jvm 会认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象之前,会先调用 finalize 方法
- 垃圾回收机制的调用,是由系统来决定 (即有自己的 GC 算法),也可以通过 System.gc () 主动触发垃圾回收机制
# 断点调试
- 在断点调试过程中,是运行状态,是以对象的运行类型来执行的
# 快捷键
F7(跳入) shift+F8(跳出) F9(resume,执行到下一个断点)
F7:跳入方法内 F8:逐行执行代码 shift+F8:跳出方法
# 类变量和类方法
类变量和类方法也叫做静态变量和静态方法,可以直接使用而不需要初始化,对所有对象都共享。
- 在 java7 以上,静态变量和静态方法都是储存在方法区;java7 及以下的静态变量储存在常量池的一个专门储存 static 变量的地方
- 类方法中不能使用和对象有关的关键字,比如 this 和 super
- 静态方法只能访问静态变量或方法
- 静态方法不能被重写
# main 方法语法
- main 方法接受 String 类型的数组参数,该数组中保存执行 java 命令时传递给所运行的类的参数
1 | //语法:java 执行的程序 参数1 参数2 参数3 |
# 代码块
代码块又称为初始化块,属于类中的成员,是类的一部分,类似于方法,将逻辑语句封装在方法体中,用 {} 包围。但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显示调用,而是加载类时,或创建对象时隐式调用。
1 | //基本语法 |
- 修饰符可选,要写的话也只能写 static
- 代码块分为两类,使用 static 修饰的交静态代码块,没有 static 修饰的交普通代码块
- 逻辑语句可分为任何逻辑语句(输入输出、方法调用、循环、判断等)
- 分号可以省略
# 代码块的好处
- 相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作
- 如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的复用性
1 | class Movie{ |
# 细节
- static 代码块的作用是对类进行初始化,而且随着类的加载而执行,并且只会执行一次。如果是普通代码块,每创建一个对象,就执行一次
类什么时候被加载:
- 创建实例时(new)
- 创建子类对象实例,父类也会被加载,父类先加载,子类后加载
- 使用类的静态成员时(静态变量 / 方法)
-
普通代码块在创建对象实例时会被隐式的调用,被创建一次就会被调用一次。但如果只是使用类的静态成员,普通代码块不会执行
-
创建对象时,代码块在类中的调用顺序如下 1->2->3
- 调用静态代码块和静态属性初始化(静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按他们定义的顺序调用)
- 调用普通代码块和普通属性的初始化(普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和普通属性初始化,则按定义顺序调用)
- 调用构造方法
-
构造器的最前面其实隐含了 super () 和调用普通代码块的一句话,静态相关的代码块,属性初始化,在类加载时就执行完毕,因此是优先于构造器和普通代码块执行的
1 | class A { |
- 创建一个子类对象时的加载顺序:(面试高频)
- 父类的静态代码块和静态属性
- 子类的静态代码块和静态属性
- 父类的普通代码块和普通属性初始化
- 父类的构造方法
- 子类的普通代码块和普通属性初始化
- 子类的构造方法
# 单例设计模式
单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。单例模式有两种方式:饿汉式和懒汉式
# 饿汉式
无论是否使用对象,都会先创建,所以叫饿汉式。但这样有可能会造成资源浪费
步骤如下
- 构造器私有化(防止直接 new
- 类的内部创建对象
- 向外暴露一个静态的公共方法 getInstance
1 | public class SingleTon { |
# 懒汉式
在需要使用的时候才创建对象,所以叫懒汉式
步骤如下:
- 构造器私有化
- 定义一个 static 对象
- 提供一个 public 方法,可以返回 Cat 对象
1 | public class SingleTon2 { |
# 两者区别
- 最主要的区别在于创建对象的时机不同,饿汉式是在类加载就创建了对象实例,而懒汉式是在使用是才创建
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题
- 饿汉式存在浪费资源的可能,懒汉式因为是使用时才创建,不存在这个问题
- 在 javaSE 标准类中,java.lang.Runtime 就是经典的单例模式
# final 关键字
final 可以修饰类、属性、方法和局部变量,在什么时候会用到 final 关键字
- 当不希望类被继承时,可以用 final 修饰
- 当不希望父类的某个方法被子类覆盖 / 重写时
- 当不希望类的某个属性的值被修改
- 当不希望某个局部变量被修改
# 注意事项
- final 修饰的属性又叫常量,一般用 XX_XX_XX 来命名
1 | public final double TAX_RATE = 0.08; |
- final 修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在以下位置之一
- 定义时:如 public final double TAX_RATE = 0.08;
- 在构造器中
- 在代码块中
- 如果 final 修饰的属性是静态的,则初始化的位置只能是:定义时;在静态代码块;不能在构造器中赋值
- final 类不能继承,但可以实例化对象
- 如果类不是 final 类,但是含有 final 方法,则该方法虽然不能重写,但是可以被继承
- 一般来说,如果一个类已经是 final 类了,就没必要将方法再修饰成 final
- final 不能修饰构造器
- final 和 static 往往搭配使用,效率更高,底层编译器做了优化处理,不会导致类加载
- 包装类(integer,double,float,boolean 等都是 final),String 也是 final 类
# 抽象类
当父类的某些方法需要声明,但是又不确定如何实现时,可以将其声明为抽象方法,那么这个类就是抽象类
一般情况都是让子类继承来实现方法,抽象类的价值更多是在于设计,设计者设计好之后,让子类继承并实现抽象类。抽象类是考官比较爱问的知识点,在框架和设计模式使用较多
1 | //当一个类中存在抽象方法时,类也要声明为抽象类 |
# 细节
- 抽象类不能被实例化
- 抽象类不一定要包含 abstract 方法,抽象类可以没 abstract 方法
- 一旦类包含了 abstract 方法,这个类就必须声明为抽象类
- abstract 只能修饰类和方法,不能修饰属性或其他的
- 抽象类可以有任意成员,比如非抽象方法、构造器、静态属性等
- 抽象方法不能有主题,即不能实现
- 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为抽象类
- 抽象方法不能使用 private、final 和 static 修饰,因为这些关键字和重写都是相违背的
# 抽象模板模式
在模板模式中,一个抽象类公开定义了执行它的方法的方式,它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行,这种类型的设计模式属于行为型模式。
# 主要解决的问题
解决在多个子类中重复实现相同的方法的问题,通过将通用方法抽象到父类中来避免代码重复
** 使用建议:** 当有多个子类共有的方法且逻辑相同时,考虑使用模板方法定义在父类中;对于重要或复杂的方法,可以考虑作为模板方法定义在父类中
# 优缺点
- ** 封装不变部分:** 算法的不变部分被封装在父类中
- ** 扩展可变部分:** 子类可以扩展或修改算法的可变部分
- ** 提取公共代码:** 减少代码重复,便于维护
缺点就是 ** 类数目增加:** 没个不同的实现都需要一个子类,可能倒是系统庞大
# 实例
1 | //创建一个抽象类,它的模板方法被设置为final |
1 | //创建扩展了上述类的实体类 |
# 接口
接口就是给出一些没有实现的方法,到某个类要使用的时候,再根据具体情况把这些方法写出来
1 | interface 接口名{ |
- 在 jdk7.0 前,接口里的所有方法都没有方法体
- jdk8.0 后接口类可以有静态方法和默认方法 (static 和 default),也就是说接口中可以有方法的具体实现
1 | /* 文件名 : Animal.java */ |
1 | /* 文件名 : MammalInt.java */ |
# 细节
- 接口不能被实例化
- 接口中所有的方法都是 public 方法,如果不写也是默认 public,接口中抽象方法可以不用 abstract 修饰
- 一个普通类实现接口,就必须将该接口的所有方法都实现(alt+enter 快捷键)
- 抽象类实现接口,可以不用实现接口的方法
- 一个类可以同时实现多个接口
- 接口中的属性只能是 final 类型,而且是 public static final 修饰符,但是被隐藏了,比如 int a=1; 实际上是 public static final int a=1;(必须初始化)
- 接口中书信固定访问形式:接口名。属性名
- 一个接口不能继承其他的类,但是可以继承多个别的接口:interface A extends B,C {}
- 接口的修饰符只能是 public 和默认,这点和类的修饰符是一样的
# 接口和继承的比较
当子类继承了父类,就自动拥有了父类的功能。如果子类需要扩展功能,可以通过实现接口的方式来扩展。
可以理解为,实现接口是对 java 单继承机制的一种补充。
-
继承的作用主要在于:解决代码的复用性和可维护性
-
而接口的作用在于:设计。设计好各种规范,让类来实现这些方法
-
接口比继承更加灵活,继承是满足 is - a 的关系,而接口秩序满足 like - a 的关系
-
接口在一定程度上实现代码解耦
# 接口的多态
- 多态参数
1 | //实现了这个接口的类的对象,都可以作为形参传递 |
- 多态数组
1 | public class AAA{ |
- 接口存在多态传递现象
1 | public class AA{ |
# 内部类
一个类的内部又完整的嵌套类另一个类结构。被嵌套的类称为内部类 (inner class),嵌套其他类的类称为外部类 (outer class),是类的第五大成员 (属性、方法、构造器、代码块、内部类),内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。
1 | class Outer{//外部类 |
# 内部类的分类
- 定义在外部类局部位置上(比如方法内):
- 局部内部类(有类名)
- 匿名内部类(没有类名,重点)
- 定义在外部类的成员位置上:
- 成员内部类(没用 static 修饰)
- 静态内部类(使用 static 修饰)
# 局部内部类的使用
局部内部类是定义在外部类的局部位置,比如方法中,并且有类名
-
局部内部类可以直接访问外部类的所有成员,包括私有成员
-
不能添加访问修饰符,因为他的低位就是应该局部变量,局部变量是不能使用修饰符的,但是可以使用 final 修饰,因为局部变量也可以使用 final
-
作用域:只在定义它的方法或代码中
-
局部内部类访问外部类的成员,可以直接访问
-
外部类访问局部类的成员,需要创建对象再访问
-
外部其他类不能访问局部内部类,因为局部内部类地位是一个局部变量
-
如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this. 成员)去访问
-
System.out.println("外部类的n=" + 外部类名.this.n);
<!--code59-->
-
匿名内部类是系统自动生成的,表面上以一个对象的形式存在,但实际上底层生成了一个类,比如外部类叫做 Outer,那么匿名内部类会叫做 Outer$1,如果后面还有匿名内部类就依次按顺序排号
1 | class Outer { |
实际上他的形式在底层是这样的
1 | class Outer$1 extends Person {} |
# 细节
- 匿名内部类的语法比较奇特,因为匿名内部类既是一个类的定义,同时本身也是一个对象,因此从语法上看,它既有定义类的特征也有创建对象的特征。所以可以直接调用匿名内部类方法
如何调用呢,以上面的为例
1 | class Outer { |
1 | class Outer { |
- 可以直接访问外部类的所有成员,包含私有的
- 不能添加访问修饰符,因为它的地位是一个局部变量
- 作用域:仅仅在定义它的方法或代码块中
- 匿名内部类访问外部类成员,直接访问
- 外部其他类不能访问匿名内部类,因为匿名内部类相当于一个局部变量
- 如果外部类和内部类的成员重名,在内部类访问时,遵循就近原则,如果想访问外部类的成员,可以使用(外部类名.this. 成员)去访问
# 实例
当作实参直接传递
1 | public class Grass { |
对比一下传统的方法(硬编码),如果说要多次使用,可以就用传统方法,但如果只用一次,直接传递会更简洁高效
1 | public class Grass { |
有一个铃声接口 Bell,里面有一个 ring 方法;有一个手机类 Cellphone,具有闹钟功能 alarmclock,参数是 Bell 类型;测试手机类的闹钟功能,通过匿名内部类作为参数,输出 "起床啦";再传入一个匿名内部类对象,输出 "get up!"
1 | public class Grass { |
# 成员内部类的使用
成员内部类是定义在外部类的成员位置,并没有 static 修饰
-
可以直接访问外部类的所有成员,包括私有的
-
可以添加任意访问修饰符(public、protected、默认、private),因为它本身也是一个成员
-
作用域和外部类的其他成员一样为整个类体,在外部类的成员方法中创建成员内部类对象,再调用方法
-
成员内部类访问外部类,直接访问
-
外部类访问成员内部类,创建对象再访问
-
外部其他类访问成员内部类
-
// 第一种方式,相当于把 new Inner () 当作是 Outer 的成员
Outer.Inner inner = Outer.new Inner();
<!--code67-->
-
// 第三种其实本质上就是前两种的结合
new Outer().new Inner();
<!--code68-->
-
-
可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员
-
可以添加任意访问修饰符(public、protected、默认、private),因为它的地位就是一个成员
-
作用域:同其他的成员,为整个类体
-
静态内部类访问外部类,可以直接访问所有静态成员
-
外部类访问静态内部类,创建对象再访问
-
外部其他类访问静态内部类
1
Outer.Inner inner = new Outer.Inner();
-
如果外部类和内部类的成员重名,在内部类访问时,遵循就近原则,如果想访问外部类的成员,可以使用(外部类名。成员)去访问
# 枚举类
枚举是一组常量的集合,属于一种特殊的类,里面只包含一组有限的特定的对象
- 不需要提供 setXxx 方法,因为枚举对象值通常为只读
- 对枚举对象 / 属性使用 final+static 共同修饰,实现底层优化
- 枚举对象名通常使用全部大写,常量的命名规范
- 枚举对象根据需要,也可以有多个属性
# 实例
-
用自定义类来实现枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39public class Grass {
public static void main(String[] args) {
System.out.println(Season.SPRING);
System.out.println(Season.SUMMER);
System.out.println(Season.AUTUMN);
System.out.println(Season.WINTER);
}
}
class Season {
private String name;
private String desc;
public static final Season SPRING = new Season("春天", "温暖");
public static final Season SUMMER = new Season("夏天", "炎热");
public static final Season AUTUMN = new Season("秋天", "凉爽");
public static final Season WINTER = new Season("冬天", "寒冷");
private Season(String name, String desc) {
this.name = name;
this.desc = desc;
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", desc='" + desc + '\'' +
'}';
}
} -
使用 enum 关键字实现枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public class Grass2 {
public static void main(String[] args) {
System.out.println(Season.SPRING);
System.out.println(Season.SUMMER);
System.out.println(Season.AUTUMN);
System.out.println(Season.WINTER);
}
}
enum Season2 {
//使用关键字enum代替class
//如果有多个常量(对象),用逗号间隔
//格式:常量名(实参列表)
//如果使用enum来实现枚举,要求将定义常量对象写在最前面
SPRING("春天", "温暖"),SUMMER("夏天", "炎热"),
AUTUMN("秋天", "凉爽"),WINTER("冬天", "寒冷"),WHAT;
private String name;
private String desc;
private Season2{}//无参构造器,上面的WHAT因为没有传入参数所以是调用的无参构造器
private Season2(String name, String desc) {
this.name = name;
this.desc = desc;
}
public String getName() {
return name;
}
public String getDesc() {
return desc;
}
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", desc='" + desc + '\'' +
'}';
}
}
# 细节
- 当使用 enum 关键字开发一个枚举类时,默认会继承 Enum 类,且是一个 final 类(可以通过反编译看到)
- 传统的 public static final Season SPRING = new Season (“春天”, “温暖”); 简化成 SPRING (“春天”, “温暖”),这里必须知道它调用的时哪个构造器
- 如果使用无参构造器创建枚举对象,则实参列表和小括号都可以省略
- 当有多个枚举对象时,使用逗号间隔,分号结尾
- 枚举对象必须放在枚举类的行首
# enum 常用方法
因为 enum 会默认继承 Enum 类,在父类中有些方法可以用
- toString:Enum 类已经重写过了,返回的是当前对象的属性信息
- name:返回当前对象名(常量名),子类中不能重写
- ordinal:返回当前对象的位置号,默认从 0 开始
- values:返回当前枚举类中所有的常量
- valueOf:将字符串转换成枚举对象,要求字符串必须为已有的常量名,否则报异常
- compareTo:比较两个枚举常量,比较的就是位置号。(位置编号相减得 0 为真)
# enum 实现接口
-
使用 enum 关键字后,不能再继承其他类,因为 enum 类会隐式继承 Enum,而 java 是单继承机制
-
枚举类和普通类一样,可以实现接口
1
enum 类名 implements 接口1, 接口2{}
# 注解
- 注解(Annotation)也被称为元数据(Metadata),用于修饰包、类、方法、属性、构造器、局部变量等数据信息
- 和注释一样,注解不影响程序逻辑,但注解可以被编译或运行,相当于嵌入在代码中的补充信息
- 再 javase 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 javaEE 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替 java EE 旧版中所遗留的繁冗代码和 XML 配置等。
使用 Annotation 时要在其前面加 @符号,并把该 Annotation 当成一个修饰符使用,用于修饰它支持的程序元素
三个基本的 Annotation:
- @Override:限定某个方法,是重写父类方法,该注解只能用于方法
- @Deprecated:用于表示某个程序元素(类、方法等)已过时
- @SuppressWarnings:抑制编译器的警告
# @Override
- @Override 只能修饰方法,不能修饰其他类、包、属性等
- @Target 是修饰注解的注解,称为元注解
- 查看 @Override 注解的源码为 @Target (ElementType.METHOD),说明只能修饰方法
1 | class Father{ |
# @Deprecated
- 用于表示某个程序元素(类、方法、属性等)已过时
- 可以修饰方法、类、字段、包、参数等
- @Deprecated 的作用可以做到新旧版本的兼容和过渡
- @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
1 | public class Grass2 { |
# @SuppressWarnings
有时候写代码,代码其实可以正常运行,但是会有一些警告,这个时候就可以用 @SuppressWarnings 注解来抑制警告信息
1 |
|
- @SuppressWarnings 的作用范围和放置的位置相关,比如放在 main 方法,那么抑制的范围就是 main 方法
# 异常 Exception
快捷键:ctrl + alt + t 选择 try-catch
# 异常的概念
一般可以分为两类
- Error:java 虚拟机无法解决的严重问题,如:JVM 系统内部错误、资源耗尽等严重情况。比如 StackOverflow [栈溢出] 和 OOM (out of memory),Error 是严重错误,程序会崩溃
- Exception:其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如空指针方向,试图读取不存在的文件,网络连接中断等等,Exception 分为两大类:运行时异常和编译时异常
# 异常体系图
- 异常分为两大类,运行时异常和编译时异常
- 运行时异常编译器检查不出来,一般是指编程时的逻辑错误,时程序员应该避免其出现的异常。java.lang.RuntimeException 类以及它的子类都是运行时异常
- 对于运行时异常,可以不做处理,因为这类异常很普遍,若全处理看你会对程序的可读性和运行效率产生影响
- 编译时异常时编译器要求必须处理的异常
- 常见的运行时异常:
- NullPointException 空指针异常:当应用程序试图在需要对象的地方使用 null 时,抛出该异常
- ArithmeticException 数学运算异常
- ArrayIndexOutOfBoundsException 数组下标越界异常
- ClassCastException 类型转换异常
- NumberFormatException 数字格式不正确异常
- 常见的编译异常:
- SQLException:操作数据库时,查询表可能发生异常
- IOException:操作文件时发生的异常
- FileNotFoundException:当操作一个不存在的文件时发生异常
- ClassNotFoundException:加载类,而该类不存在时异常
- EOFException:操作文件,到文件末尾,发生异常
- IllegalArgumentException:参数异常
# 异常处理概念
异常处理就是当异常发生时,对异常处理的方式
# try-catch-finally
程序员在代码中捕获发生的异常,自行处理
try 块用于包含可能出错的代码,catch 块用于处理 try 块中发生的异常,可以根据需要在程序中有多个 try-catch 模块
注意事项:
- 如果异常发生了,则异常发生后面的代码都不会执行,直接进入到 catch 块
- 如果异常没有发生,则顺序执行 try 的代码块,不会进入 catch
- 如果希望不管是否发生异常,都执行某段代码(比如关闭连接,释放资源等),则使用 finally {}
- 如果 try 代码块可能有多个异常,,可以使用多个 catch 分别捕获不同的异常,但是要求子类异常写在前面,父类一场写在后面(比如 Exception 在后,NullPointerException 在前),如果发生异常,只会匹配一个 catch
- 可以进场 try-finally 配合使用,这种用法相当于没有捕获异常,因此程序会直接崩掉。当希望执行一段代码,不管是否发生异常,都必须执行某个业务逻辑时可以使用
1 | try { |
# throws
将发生的异常抛出,交给调用者(方法)来处理,最顶级的处理者就是 JVM
如果一个方法(中的语句执行时)可能生成某种异常,但不能确定如何处理这种异常,则此方法显式地声明抛出异常,表明该方法将不对这些异常进行处理,而又该方法的调用者处理
在方法声明中用 throws 语句可以声明抛出异常的列表,throws 后面的异常类型可以是方法中产生的异常类型,也可以是它的父类
注意事项:
- 对于编译异常,程序中必须处理,比如 try-catch 或者 throws
- 对于运行时异常,程序中如果没有处理,默认就是 throws 的方式处理
- 子类重写父类的方法时,对抛出异常的规定:子类重写的方法所抛出的异常类型要么和父类抛出的一致,要么为父类抛出的异常的类型的子类型
- 在 throws 过程中,如果有方法 try-catch,就相当于处理异常,就可以不必 throws
1 | public void f1() throws Exception {//这里是Exception是父类,也可以是FileNotFoundException子类 |
# 自定义异常
当程序中出现了某些错误,但该错误信息并没有在 Throwable 子类中描述处理,这个时候可以自己设计异常类,用于描述该错误信息
自定义异常的步骤
- 定义类:自定义异常类名(程序员自己写),继承 Exception 或 RuntimeException
- 如果继承 Exception,属于编译异常
- 如果继承 RuntimeException,属于运行异常(一般来说继承 RuntimeException)
1 | public class Grass { |
# throw 和 throws 的对比
意义 | 位置 | 后面跟的东西 | |
---|---|---|---|
throws | 异常处理的一种方式 | 方法声明处 | 异常类型 |
throw | 手动生成异常对象的关键字 | 方法体中 | 异常对象 |
# 常用类
# 包装类 Wrapper
# 包装类的分类
- 针对八种基本数据类型相应的应用类型 —— 包装类
- 有了类的特点,就可以调用类中的方法
基本数据类型 | 包装类 |
---|---|
boolean | Boolean |
char | Character |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
# 包装类和基本数据类型的转换
包装类是对象,基本数据类型是属性
- jdk5 前的手动装箱和拆箱方式,装箱:基本类型 -> 包装类型,反过来就是拆箱
- jdk5 及以后的自动装箱和拆箱方式
- 自动装箱底层调用的时 valueOf 方法,比如 Integer.valueOf ()
1 | public class Grass { |
# 包装类型和 String 类型的相互转换
1 | public class Grass { |
# Integer 类和 Character 类的常用方法
这几个是比较常用的,还有非常多
1 | System.out.println(Integer.MIN_VALUE);//返回最大/最小值 |
# Integer 类
面试题:
1 | //由于生成的是两个对象,所以结果为false |
1 | Integer x = 128; |
# String 类
-
字符串的字符使用 Unicode 字符编码,一个字符(不区分字母还是汉字)占两个字节
-
String 类较常用构造器
1
2
3
4String s1 = new String();
String s2 = new String(String original);
String s3 = new String(char[] a);
String s4 = new String(char[] a, int startIndex, int count); -
String 类实现了接口 Serializable(String 可以序列化,在网络传输),还实现了 Comparable(对象可以比较大小)
-
String 是 final 类,不能被其他的类继承
-
String 有属性 private final char value []; 用于存放字符串内容,所以其本质是字符数组
-
value 是一个 final 类型,不可以修改(地址不可修改,值可以修改)
# 创建 String 对象的两种方式
-
直接赋值:
String s = "tch";
创建过程:先从常量池查看是否有 "tch" 数据空间,如果有,则直接指向它;如果没有则重写创建,然后指向。s 最终指向的是常量池的空间地址
-
调用构造器:
String s = new String("tch");
创建过程:先在堆中创建空间,里面维护了 value 属性,指向常量池的 "tch" 空间。如果常量池没有 "tch",就重新创建,如果有,就直接通过 value 指向。最终指向的是堆中的空间地址
# 实例
1 | String a = "tch"; |
1 | Person p1 = new Person(); |
# String 对象特性
- String 是一个 final 类,代表不可变的字符序列
- 字符串是不可变的,一个字符串对象一旦被分配,其内容是不可改变的
- 常量相加,看的是常量池;变量相加,看的是堆
1 | //一共有三个对象 |
# String 类的常见方法
String 类是保存字符串常量的,每次更新都需要重新开辟空间,效率较低,因此 java 设计者还提供了 StringBuilder 和 StringBuffer 来增强 String 的功能。
1 | substring(1,4);//左闭右开,截取从1到4减1的字符串 |
# StringBuffer 类
- java.lang.StringBuffer 代表可变的字符序列,可以对字符串内容进行增删
- 很多方法与 StringBuffer 相同,但 StringBuffer 是可变长度的
- StringBuffer 是一个容器
1 | StringBuffer str1 = new StringBuffer("hello"); |
# String 和 StringBuffer 的对比
- String 保存的是字符串常量,里面的值不能更改,每次 String 类的更新,实际上是更改地址,效率比较低
- StringBuffer 保存的是字符串变量,里面的值可以修改,每次 StringBuffer 的更新实际上可以更新内容,不用每次更改地址,效率较高(空间大小不够的时候会扩容,也就会更改地址)
# StingBuffer 的构造器
1 | StringBuffer() //创建一个大小为16的char[],用于存放字符内容 |
# String 和 StringBuffer 相互转换
# String —> StringBuffer
1 | String s = "helllo"; |
# StringBuffer —> String
1 | //way 1 |
# StringBuffer 类常见方法
1 | StringBuffer s = new StringBuffer("hello"); |
# StringBuilder 类
- 一个可变的字符序列。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快
- 在 StringBuilder 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据
- StringBuilder 的常用方法和 StringBuffer 是一样的
- StringBuilder 是 final 类,不能被继承
- StringBuilder 对象字符序列也是存放在父类 AbstractStringBuilder 的 char [] value 中,所以也是存放在堆中
- 多线程使用有风险,源码中没有线程同步的操作,也就是没有 synchronized 关键字
# String,StringBuffer 和 String Builder 的对比
-
String、StringBuffer 和 StringBuilder 都代表可变的字符序列,且方法一样
-
String:不可变字符序列,效率低,但是复用率高
-
StringBuffer:可变字符序列,效率较高(增删),线程安全
-
StringBuilder:可变字符序列,效率最高,但线程不安全
-
String 使用注意说明
1
2string s = "a";
s += "b";这串代码中,原本的字符串对象 "a" 是被丢弃了之后在产生的 "ab",多次执行这种改变内容的操作会导致大量副本对象存留在内存中,降低效率,尤其是当这种操作在循环中时。所以在需要对 String 对象做大量的修改时,不要用 String
# Math 类
-
abs 绝对值
1
int abs = Math.abs(-1);//1
-
pow 求幂
1
double pow = Math.pow(2, 4);//2的四次方
-
ceil 向上取整
1
double ceil = Math.ceil(-3.001);//-3.0
-
floor 向下取整
1
double floor = Math.floor(-4.99);//-5.0
-
round 四舍五入
1
long round = Math.round(-5.001);//-5
-
sqrt 开方
1
double sqrt = Math.sqrt(9.0)//3.0
-
random 求随机数
1
2
3double random = Math.random();//返回一个[0,1)之间的随机小鼠
//如果要让它取到一个设定范围内的整数,比如[2, 7],可以这么写
int random = (int)(2 + Math.random() * (7 - 2 + 1));//因为需要取整,所以右边需要加1 -
max、min 求最值
1
2int min = Math.min(1, 10);
int max = Math.max(1, 10);
# Arrays 类
Arrays 类用于管理或操作数组(比如排序、搜索)
-
toString:返回数组的字符串形式
1
Arrays.toString(arr);
-
sort 排序:自然排序和定制排序(忘记了可看 p483)
1
2
3
4
5
6
7
8
9
10
11
12
13//自然排序
Integer arr[] = {1, -1, 4, 5, 0};
Arrays.sort(arr);
//定制排序,传入一个接口Comparator实现定制排序
Arrays.sort(arr, new Comparator() {
public int compare(Object o1, Object o2) {
Integer i1 = (Integer o1);
Integer i2 = (Integer o2);
return i1 - i2;
}
});
//实现了Comparator接口的匿名内部类,要求实现compare方法 -
binarySearch:通过二分法进行查找,只能对有序数组使用
1
int index = Arrays.binarySearch(arr, 3);
-
copyOf 数组元素复制
1
Integer[] newArr = Arrays.copyOf(arr, arr.length);
-
fill:数组元素的填充
1
2Integer[] num = new Integer[] {5, 3, 1};
Arrays.fill(num, 99);//将数组用99填充 -
equals:比较两个数组的元素内容是否完全一致
1
boolean equals = Arrays.equals(arr1, arr2);
-
asList:将一组值转换成 list
1
2
3
4List<Integer> asList = Arrays.asList(2,3,4,5,6,1);//asList会将这些数据转为一个List集合
System.out.println("asList=" + asList);
//返回asList编译类型List(接口)
//asList的运行类型是java.util.Arrays$ArrayList
# System 类
-
exit:退出当前程序
-
arraycopy:复制数组元素,比较适合底层调用,一般情况还是用 Arrays.copyOf。
1
2
3
4int[] src = {1,2,3};
int[] dest = new int[3];
System.arraycopy(src, 0, dest, 0, 3);
//把src从第0个位置开始的元素,复制给dest,从0位置开始,复制3个元素 -
currentTimeMillens:返回当前时间距离 1970-1-1 的毫秒数
-
gc:运行垃圾回收机制
1
System.gc();
# BigInteger 和 BigDecimal 类
- BigInteger 适合保存比较大的整型
- BigDecimal 适合保存精度更高的浮点型?
1 | BigInteger a = new BigInteger("9999999999999999999999"); |
加减乘除不能直接用传统的方法,要用下面的方法来进行
# 常见方法
-
add
1
2BigInteger add = bigInteger.add(b);
BigDecimal add = bigdecimal.add(b); -
subtract:减
1
2BigInteger subtract = bigInteger.subtract(b);
BigDecimal subtract = bigdecimal.subtract(b); -
multiply
1
2BigInteger multiply = bigInteger.multiply(b);
BigDecimal multiply = bigdecimal.multiply(b); -
divide:除
1
2
3
4
5BigInteger divide = bigInteger.divide(b);
BigDecimal divide = bigdecimal.divide(b);
//在除法中可能会出现无限循环小数,这里可以指定精度:BigDecimal.ROUND_CELING
System.out.println("bigDecimal.divide(b, a.ROUND_CELING)")
# 日期类
- Date:精确到毫秒
- SimpleDateFormat:格式和解析日期的类。它允许进行格式化(日期 -> 文本),解析(文本 -> 日期)和规范化
1 | Date d1 = new Date(); |
# 第二代日期类
-
Calendar 类
1
public abstract class Calendar extends Object implements Serializable, Cloneable, Comparable<Calendar>
-
Calendar 类是一个抽象类,为 YEAR, MONTH, DAY_OF_MONTH, HOUR 等日历字段之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法
1 | //Calendar是一个抽象类,且构造器是私有的,所以可以通过getInstance()来获取实例 |
# 第三代日期类
JDK 1.0 中包含了一个 java.util.Date 类,但它的大多数方法已经在 JDK 1.1 引入 Calendar 类后被弃用了,而 Calendar 类也存在一些问题
- 可变性:日期和时间这样的类应该是不可变的
- 偏移性:Date 中的年份是从 1900 年开始的,而月份都从 0 开始
- 格式化:格式化只对 Date 有用,Calendar 不可以
- 线程不安全,不能处理闰秒(每隔 2 天,多出 1s)
# 第三代日期类常见方法
LocalDate (日期 / 年月日)、LocalTime (时间 / 时分秒)、LocalDateTime (日期时间) JDK8 加入
1 | LocalDateTime ldt = LocalDateTime.now(); |
# 格式日期类 DateTimeFormatter
1 | //先创建一个DateTimeFormatter对象 |
# 时间戳
类似于 Date,提供了一系列和 Date 类转换的方式
1 | Instant now = Instant.now(); |
# 更多方法
- LocalDateTime 类
- MonthDay 类:检查重复事件
- 是否是闰年
- 增加日期的某个部分
- 使用 plus 方法增加时间的某个部分
- 使用 minus 方法查看一年前和一年后的日期
# 集合(Collection)
集合主要是两组,即单列集合和双列集合。
- Collection 接口有两个重要的子接口,List 和 Set,他们实现子类都是单列集合
- Map 接口的实现子类是双列集合,存放 K-V
1 | ArrayList al = new ArrayList(); |
# 集合和数组的对比
数组:
- 长度开始时必须指定,且指定后就不能修改
- 保存的必须为同一类型的元素
- 使用数组进行增加元素的示意代码比较麻烦
而集合:
- 可以动态保存任意多个对象,使用比较方便
- 提供了一系列方便的操作对象的方法:add、remove、set、get 等
- 使用集合添加,删除新元素的示意代码,更加简洁
# Collection 接口和常用方法
# Collection 接口实现类的特点
public interface Collection<E> extends Iterable<E>
- Collection 实现子类可以存放多个元素,每个元素可以是 Object
- 有些 Collection 的实现类,可以存放重复的元素,有些不可以
- Collection 的实现类,有些是有序的(List),有些是无序的(Set)
- Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的
# Collection 接口常用方法
以实现子类 ArrayList 来演示
1 | List list = new ArrayList(); |
-
add:添加单个元素
1
2
3list.add("tch");
list.add(10);
list.add(true); -
remove:删除指定元素
1
2list.remove(0);//删除索引位置为0的元素
list.remove("tch");//指定删除 -
contains:查找元素是否存在
1
list.contains("tch");
-
size:获取元素个数
1
list.size();
-
isEmpty:判断是否为空
1
list.isEmpty();
-
clear:清空
1
list.clear();
-
addAll:添加多个元素
1
2
3ArrayList list2 = new ArrayList();
list2.add("tch2");
list.addAll(list2); -
containsAll:查找多个元素是否都存在
1
list.containsAll(list2);
-
removeAll:删除多个元素
1
list.removeAll(list2);
# Collection 接口遍历元素方式
# 使用 Iterator(迭代器)
- Iterator 对象称为迭代器,主要用于遍历 Collection 集合中的元素
- 所有实现了 Collection 接口的集合类都有一个 iterator () 方法,用以返回一个实现了 Iterator 接口的对象,即可以返回一个迭代器
- Iterator 仅用于遍历集合,Iterator 本身并不存放对象
工作原理:
1 | Iterator iterator = coll.iterator();//得到一个集合的迭代器 |
在调用 iterator.next () 方法之前必须要调用 iterator.hasNext () 进行检测,若不调用,且下一条记录无效时,直接调用 iterator.next () 会抛出 NoSuchElementException 异常
如果要再次使用,需要重置迭代器 iterator = coll.iterator();
# 使用 for 循环增强
增强 for 就是简化版的 iterator,本质一样,只能用于遍历集合或数组。底层其实就是迭代器
1 | for(Object object : col){ |
# List 接口和常用方法
List 接口是 Collection 接口的子接口
- List 集合类中元素有序(即添加顺序和取出顺序一致)且可重复
- List 集合中的每个元素都有对应的顺序索引(索引从 0 开始)
- List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
- 常用的 List 接口的实现类:ArrayList、LinkedList、Vector
1 | void add(int index, Object ele)//在index位置插入ele元素 |
List 和 ArrayList 的区别:Java 中 List 和 ArrayList 的区别及使用_java list arraylist-CSDN 博客
# ArrayList 的底层结构和源码分析
# 注意事项
- permits all elements (可以放所有元素), including null。ArrayList 可以加入 null,并且可以加入多个
- ArrayList 是由数组来实现数据存储的
- ArrayList 基本等同于 Vector,除了 ArrayList 是线程不安全(但执行效率高),但在多线程情况下不建议使用 ArrayList
# 源码分析
- ArrayList 中维护了一个 Object 类型的数组 elementData. [debug 看源码]transient Objectil elementData;
- 当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0,第 1 次添加,则扩容 elementData 为 10,如需要再次扩容,则扩容 elementData 为 1.5 倍
- 如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为 1.5 倍
# Vector 的底层结构和源码分析
# 注意事项
-
Vector 类的定义说明
1
2public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable -
Vector 底层也是一个对象数组,
protected Object[] elementData;
-
Vector 是线程同步的,即线程安全,Vector 类的操作方法带有 synchronized
-
在开发中需要线程同步安全的,考虑使用 Vector
# 源码分析
- 创建对象时,如果使用的无参构造器,默认容量为 10,满了之后按照 2 倍扩容
- 创建对象时,如果是指定大小,则每次直接按照 2 倍扩容
# LinkList 的底层结构
- LinkList 实现了双向链表和双端队列的特点
- 可以添加任意元素(可重复),包括 null
- 线程不安全,没有实现同步
# 底层操作机制
- LinkList 底层是一个双向链表
- LinkList 中维护了两个属性 first 和 last 分别指向首结点和尾结点
- 每个结点 (Node 对象) 里面有维护了 prev、next、item 三个属性,其中 prev 指向前一个,next 指向后一个,实现双向链表
- 所以 LinkList 的元素的添加和删除,不是通过数组完成的,效率较高
# ArrayList 和 LinkedList 的比较
如何选择:
- 改查多,选 ArrayList
- 增删多,选 LinkedList
- 一般来说在程序中,80%-90%都是查询,因此大部分情况下会选择 ArrayList
- 在一个项目中根据业务灵活选择,也有可能一个模块使用的是 ArrayList,另一个模块是 LinkedList
# Set 接口和常用方法
- 无序,没有索引
- 不允许重复元素,所以最多包含一个 null
- JDK API 中 set 接口的实现类有 HashSet、TreeSet 等等
- set 接口对象不能通过索引来获取,且没有 get 方法,所以不能用普通 for 循环遍历
# HashSet
- HashSet 实现了 Set 接口,可存放 null 但只能有一个
- HashSet 实际上是 HashMap
- HashSet 不抱着元素是有序的,取决于 hash 后,在确定索引的结果
- 不能有重复对象
# 方法注意事项
- add 方法在执行后会返回一个 boolean 值,如果添加成功返回 true,反之返回 false
- 可以通过 remove 指定删除对象
# HashSet 底层机制
HashSet 底层是 HashMap,HashMap 底层是数组 + 链表 + 红黑树
- 添加一个元素时,先得到 hash 值,再转成索引值
- 找到储存数据表 table,看这个索引位置是否已经存放的有元素
- 如果没有直接加入
- 如果有,就调用 equals 比较,如果相同就放弃添加,反之添加到最后
- 在 java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认是 8),且 table 的大小 >=MIN_TREEIFY_CAPACITY (默认 64),就会进行树化(红黑树)
扩容机制:
- 第一次添加时,table 扩容到 16,临界值 (threshold) 是 16 * 加载因子 (loadFactor),加载因子 = 0.75,结果就是 12
- 如果 table 数组使用到了临界值 12,就会扩容到
16*2=32
,新的临界值就是32*0.75
,以此类推 - 在 java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认是 8),并且 table 的大小 >=MIN_TREEIFY_CAPACITY (默认 64),就会进行树化(红黑树),否则仍然采用数组扩容机制
- 要注意的是,在一个 table 中只要添加了 0.75 倍的 Node,就会进行扩容,无论是添加到某个结点,还是某个结点的链表上
# LinkedHashSet
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个数组 + 双向链表
- LinkedHashSet 根据元素的 hashCode 值来决定元素的储存位置,同时使用链表维护元素的次序 (图),这使得元素看起来是以插入顺序保存的
- LinkedHashSet 不允许添加重复元素
# TreeSet
//////
# 图(Map)
1 | Map map = new HashMap(); |
Map 接口实现类的特点:
- Map 与 Collection 并列存在,用于保存具有映射关系的数据:Key-Value
- Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
- Map 中的 key 不允许重复,原因和 HashSet 一样(当 key 相同时,新的那个 key 的 value 会替换旧的 key 的 value)
- Map 中的 value 可以重复
- Map 的 key 可以为 null,value 也可以为 null,但 key 为 null 只能有一个,而 value 可以有多个
- 常用 String 类作为 Map 的 key
- key 和 value 之间存在单向一对一关系,即通过指定的 key 能找到对应的 value(用 get 方法,通过 key 值查找)
- 一对 k-v 是放在一个 Node 中的,因为 Node 实现了 Entry 接口(有些书上说,一对 k-v 就是一个 Entry)
# 对 Map.Entry 的解释
Map.Entry 是 Map 中的一个接口,他的用途是表示一个映射项(里面有 Key 和 Value),而 Set<Map.Entry<K,V>> 表示一个映射项的 Set。Map.Entry 里有相应的 getKey 和 getValue 方法,即 JavaBean,让我们能够从一个项中取出 Key 和 Value。
因为 Map 这个类没有继承 Iterable 接口,所以不能直接通过 map.iterator 来遍历,所以就只能先转化为 set 类型,用 entrySet () 方法,其中 set 中的每一个元素值就是 map 中的一个键值对,也就是 Map.Entry<K,V> 了,然后就可以遍历了。
Map 是 java 中的接口,Map.Entry 是 Map 的一个内部接口。
Map 提供了一些常用方法,如 keySet ()、entrySet () 等方法,keySet () 方法返回值是 Map 中 key 值的集合;entrySet () 的返回值也是返回一个 Set 集合,此集合的类型为 Map.Entry。
Map.Entry 是 Map 声明的一个内部接口,此接口为泛型,定义为 Entry<K,V>。它表示 Map 中的一个实体(一个 key-value 对)。接口中有 getKey (),getValue 方法
Node<K,V> 实现了 Entry 接口,Entry 接口中 K 表示 key, 即键,V 表示 value, 即值。Entry 即 Node 是 Map 集合中的一个对象元素,而 Map 集合正是由一个个 Entry 即 Node 对象所构成。
正是因为 Node 实现了 Entry 接口,所以使用 Entry 的时候也可以使用其 getValue () 和 getKey () 方法
Java 中 Map.Entry 详解_java map.entry-CSDN 博客
# Map 接口的常用方法
- put:添加
- remove:根据键删除
- get:根据键获取
- size:元素个数
- isEmpty:判断是否为空
- clear:清除
- containsKey:查找键是否存在
# Map 接口遍历方法
-
containsKey:查找键是否存在
-
keySet:获取所有键
1
2
3
4
5
6
7
8
9
10
11
12//第一组:先取出所有key,再通过key取出对应的value
Set set = map.keySet();
//(1)增强for
for (Object key : set) {
System.out.println(key + ":" + map.get(key));
}
//(2)迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next + ":" + map.get(next));
} -
values:获取所有值
1
2
3
4
5
6
7
8
9
10
11
12//第二组:取出所有的value
Collection values = map.values();
//(1)增强for
for (Object value : values) {
System.out.println(value);
}
//(2)迭代器
Iterator iterator2 = values.iterator();
while (iterator2.hasNext()) {
Object value = iterator2.next();
System.out.println(value);
} -
entrySet:获取所有关系 k-v
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//第三组:通过EntrySet获取k-v
Set entrySet = map.entrySet();//EntrySet<Map.Entry<K,V>>
//(1)增强for
for (Object entry : entrySet) {
//将entry对象转成Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + ":" + m.getValue());
}
//(2)迭代器
Iterator iterator3 = entrySet.iterator();
while (iterator3.hasNext()) {
Object entry = iterator3.next();
//向下转型Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + ":" + m.getValue());
}
# Hashtable
- 存放元素是键值对:k-v
- hashtable 的键和值哦都不能为 null
- hashTable 使用方法基本和 HashMap 一样
- hashTable 线程安全,hashMap 线程不安全
# 底层
- 底层数组 Hashtable$Entry [] 初始化大小为 11
- 临界值 threshold 8 = 11 * 0.75
- 用方法 addEntry (hash, key, value, index) 添加 k-v,封装到 entry
- 当 if (count>= threshold) 满足时进行扩容
# Properties
- properties 类继承自 Hashtable 类,且实现了 Map 接口,也是使用键值对来储存数据
- 使用特点和 Hashtable 类似
- properties 可以用于从 xxx.properties 文件中加载数据到 Properties 类对象,并进行读取和修改