Java基础--秋招笔记
字节码的优点
运行效率高
平台无关性
Java代码从源码到运行的过程
Java源码 --> 编译器 --> jvm可执行的Java字节码 --> jvm中的解释器 --> 机器可执行的二进制字节码 --> 程序运行
this关键字用法
1.引用类本身
2.成员变量名与形参名相同,使用this指代成员属性
3.引用本类的构造函数
super关键字的用法
1.指向当前对象的父类
2.子类中的成员变量或成员方法与父类中的成员变量或方法同名时用super指向父类的成员变量和方法
3.引用父类的构造函数。
static
作用:
1.被static修饰的变量或方法是独立于该类的任何对象,即这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享
2.被static修饰的部分会在类第一次使用时加载并进行初始化,后面根据需要可再次赋值。
3.static变量值在类加载的时候分配空间,后面创建对象是不会重新分配。但是可以赋值。
4.被static修饰的变量或方法是优于对象存在的,即一个类加载完毕之后,即使没有创建对象也可以访问。
static应用场景
1、修饰成员变量
2、修饰成员方法
3、静态代码块
4、修饰类
5、静态导包
static注意事项
1、静态只能访问静态
2、非静态既可以访问静态也可以访问非静态
流程控制语句
break:跳出上一层循环,不再执行循环。
continue:跳出本次循环,执行下一次循环
return:程序返回,不再执行下面的代码
跳出多重嵌套循环
在循环语句前定义一个标号,在循环体的代码中使用带有标号break的语句。
面向对象与 面向过程
面向过程:
优点:性能好
缺点:不易维护、复用、扩展
面向对象:
优点:易维护、复用、扩展
缺点: 性能比面向过程低
面向过程是流程化,
面向对象是模型化。
面向对象三大特性
继承
使用已存在的类的定义作为基础建立新类,可以新增数据和功能、继承父类的数据和功能,但是不能选择性继承。
子类拥有父类非private属性和方法
子类可以对父类惊醒扩展
子类可以用自己的方法实现父类的方法
封装
把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法
多态
父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。
实现方式:
继承
接口
实现形式:
重写:运行时多态
重载:编译时多态
面向对象五大基本原则
1.单一职责原则
一个类只负责一项职责
2.开放封闭原则
对扩展开放,对修改封闭
3.里氏替换原则
子类对象可以替代父类对象,且程序行为不会发生改变
4.依赖倒置原则
高层次的模块不应该依赖与低层次的模块
5.接口分离原则
不同的功能要拆分成不同的接口,不应该放在同一个接口中
抽象类与接口对比
抽象类
用来捕捉子类的通用特性。抽象类是对类的抽象,是一种模板设计。
接口
是抽象方法的集合。接口是行为的抽象,是一种行为的规范。
相同点
-
接口和抽象类都不能实例化
-
都位于继承的顶端,用于被其他继承或实现
-
都包含抽象方法,其子类都必须复写这些抽象方法
不同点
参数 | 抽象类 | 接口 |
---|---|---|
声明 | abstract | interface |
实现 | 子类使用extends关键字继承; 如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口;需要提供接口中所有声明的方法的实现 |
构造器 | 可以有构造器 | 不能有构造器 |
访问修饰符 | 抽象类中的方法可以是任意修饰符 | 接口方法默认public。且不能被定义为private或者protected |
多继承 | 一个类只能继承一个抽象类 | 一个类可以实现多个接口 |
字段声明 | 字段声明是任意的 | 字段声明默认是static和final |
抽象类可以用final修饰吗
不能,定义抽象类就是让其他类继承,定义为final的类不能被继承
创建一个对象使用什么关键字?
new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
一个对象引用可以指向0个或1个对象;一个对象可以有n个引用指向它
变量与方法
变量:在程序执行过程中,在某个范围内其值可以改变的量。从本质上讲,变量其实是内存中的一小块区域。
成员变量:方法外部,类内部定义的变量
局部变量:类的方法中的变量
成员变量与局部变量的区别
成员变量 | 局部变量 | |
---|---|---|
作用域 | 对整个类有效 | 某个范围有效(一般指方法语体内) |
存储位置 | 堆内存 | 占内存 |
生命周期 | 随对象创建而存在,随对象消失而消失 | 方法调用完或语句结束后自动释放 |
初始值 | 有默认初始值 | 无默认初始值 |
Java中定义一个不做事且没有参数的构造方法的作用
在执行子类构造方法之前会先调用此方法,帮助子类完成初始化
类的构造方法的作用
完成对类对象的初始化工作
一个类即使没有声明构造方法也会有默认的不带参数的构造方法
构造方法的特征
-
方法名与类名相同
-
没有返回值,但是不能用void声明函数
-
生成类的对象时自动执行,无需调用
静态变量与实例变量的区别
静态变量:属于类的变量,内存中只会有一份,在类加载过程中,jvm会为静态变量分配一次内存
实例变量:属于对象的变量,在创建对象时为实例变量分配内存空间
静态方法与实例方法的区别
静态方法 | 实例方法 | |
---|---|---|
外部调用 | 可以使用“类名.方法名”,和“对象名.方法名”;调用静态方法可以不创建对象 | 只能使用"对象名.方法名";调用方法 必须创建对象 |
访问本类成员 | 只能访问静态成员 | 没有限制 |
方法返回值的定义与作用
定义:我们所获得到的某个方法体中的代码执行后产生的结果
作用:接受结果,使得它可以用于其他操作。
内部内定义
将一个类的定义放在另一个类的内部
内部类本身就是类的一个属性,与其他属性定义方式一致
内部类分类
分类 | 定义 |
---|---|
静态内部类 | 定义在类内部的静态类 |
成员内部类 | 类内部,成员位置上的非静态类 |
局部内部类 | 定义在方法中的内部类 |
匿名内部类 | 没有名字的内部类 |
匿名内部类特点
-
必须继承一个抽象类或者实现一个接口
-
不能定义任何静态成员和静态方法
-
当所在的方法的形参需要被匿名内部类使用时,这个参数必须声明为final
-
可以很方便的定义回调
内部类使用场景
-
多算法场合
-
解决非面向对象的语句块
-
适当使用内部类,使代码更灵活和富有扩展性
-
当某个类除了他的外部类,不再被其他类使用时
局部内部类和匿名内部类访问局部变量时,为什么必须加上final
生命周期不一致
局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就会被销毁,而局部内部类对局部变量的引用依然存在。
为了解决这个问题,就将局部变量复制(copy)一份,作为内部类的成员变量,加上final可以保证复制的局部变量和原变量一致。
加了final可以确保局部内部类使用的变量与外层的局部变量区分开
构造器(constructor)(构造方法)是否可以被重写
构造器不能被继承,因此不能被重写,但是可以被重载
重载(overload)和重写(override)的区别
重载:发生在同一个类中,方法名相同,参数列表不同,与方法返回值和修饰符无关
重写:发生在父子类中,方法名和参数列表相同,返回值小于父类,抛出的异常小于父类,访问修饰符大于父类,
如果父类访问修饰符是private则子类中就不是重写
==和equals()
==:
-
基础数据类型比较值,
-
引用数据类型比较内存地址
eqals():
-
类没有覆盖equals(),比较内存地址
-
类覆盖了equals(),比较对象的内容
hashCode()和equals
hashCode,获取哈希码(散列码),返回一个int整数,
作用是确认该对象确认该对象在哈希表中的索引位置,
两个对象相等,hashCode一定也相同,对两个对象都调用equals方法都返回true
两个对象hashcode相等,但这两个对象不一定相等
值传递与引用传递
JDK常见包
-
java.lang:系统的基础类
-
java.io:输入输出有关类,如文件操作
-
java.nio:完善io包中的功能,提高io包的性能
-
java.net:网络有关类
-
java.util:系统辅助类,特别是集合类
-
java.sql:数据库操作的类
包Java和javax的区别
Java中的IO流分类
-
inputStream/Reader:所i有输入流的基类,分别是,字节输入流、字符输入流
-
outputStream/Writer:所有输出流的基类,分别是字节输出流、字符输出流
-
按操作方式分类
-
按照操作对象分类
BIO、NIO、AIO的区别
-
BIO:同步阻塞IO,数据的读写必须阻塞在一个线程内等待其完成。
-
优点:编程模型简单、不用考虑过载限流问题
-
缺点:不能处理高并发请求
-
-
NIO:同步非阻塞IO,客户端服务端通过Channel(通道)通讯,实现多路复用
-
优点:性能好,能适应高负载、高并发的环境
-
缺点:模型复杂,不易维护
-
-
AIO:异步非阻塞IO,异步IO是基于事件和回调机制实现,应用操作完之后会直接返回,当后台处理完成,操作系统会提醒后续的线程进行后续的操作
Files的常用方法
-
Files.exists():检测文件路径是否存在
-
Files.creatFile():创建文件
-
Files.createDirectory():创建文件夹
-
Files.copy():复制文件
-
Files.move():移动文件
-
Files.size():查看文件个数
-
Files.read():读取文件
-
Files.write():写入文件
反射的定义
:在运行状态中,对于任意一个类,都能够知道这个类的属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取信息以及动态调用对象的方法的功能称为:Java语言的反射机制
静态编译
:编译时确定类型,绑定对象
动态编译:
运行时确定类型,绑定对象
反射机制的优缺点
优点:动态加载类,提高代码灵活度
缺点:性能瓶颈,反射相当于一系列解释操作,通知JVM要做的事,比直接写代码慢很多
反射机制应用场景
模块化开发、动态代理设计、Spring,Hibernate等框架都有使用反射机制
举例:
-
使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序
-
spring框架的xml配置模式。spring通过xml配置模式装载Bean的过程
-
将程序内所有的xml或properties配置文件加载到内存中
-
Java类里面解析xml或properties里面的内容,得到对应实体的字节码字符串以及相关属性信息
-
使用反射机制,根据这个字符串获得某个类的class实例
-
动态配置实例的属性
-
Java获取反射的三种方式
-
new对象
-
通过路径
-
通过类名
1 public class Student { 2 private int id; 3 String name; 4 protected boolean sex; 5 public float score; 6 } 1 public class Get { 2 //获取反射机制三种方式 3 public static void main(String[] args) throws ClassNotFoundException { 4 //方式一(通过建立对象) 5 Student stu = new Student(); 6 Class classobj1 = stu.getClass(); 7 System.out.println(classobj1.getName()); 8 //方式二(所在通过路径-相对路径) 9 Class classobj2 = Class.forName("fanshe.Student"); 10 System.out.println(classobj2.getName()); 11 //方式三(通过类名) 12 Class classobj3 = Student.class; 13 System.out.println(classobj3.getName()); 14 } 15 }
反射使用步骤
-
获取想要操作类的对象,它是反射的核心,通过class对象我们可以调用任意方法
-
调用class类中的方法,就是反射的使用阶段
-
使用反射API操作这些信息
Java反射Api
反射API用来生成JVM中的类、接口或对象的信息
-
Class类:反射的核心类,可以获取类的属性,方法等信息
-
Field类,java.lang.reflec包中的类,表示类的成员变量,可以用来获取和设置类中的属性值
-
Method类:java.lang.reflec包中的类,表示类的方法,可以获取类中的方法信息或者执行方法
-
Constructor类:Java.lang.reflec包中的方法,表示类的构造方法
Sting相关
字符型常量与字符串常量的区别
-
形式上:字符常量是单引号引起的一个字符,字符串常量是双引号引起的若干字符
-
含以上:字符常量相当于一个整型值(ASCII值),可以参与表达式运算,字符串常量代表一个地址值(该字符串在内存中存放的位置)
-
占内存大小:字符常量只占一个字节;字符串常量占若干字节
字符串常量池
字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存使用率,避免开辟多块空间存储相同的字符串,在创建字符串时JVM会先检查字符串常量池,如果该字符串已存在池中,则返回他的引用
文本数据char
char只能表示单个字符,比如“你好”就是长度为2 的数组char[] chars = {'你',’好‘}
String特性
-
不变性:String是只读(immutable)字符串,对他进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性
-
常量池优化:String对象创建之后,会在字符串常量池中缓存
-
final:使用final定义string,表示string类不可继承,提高系统的安全性
string 不可变,但是string的引用可以改变
字符串反转
stringbuilder.reserve()
stringbuffer.reserve()
String获取长度
string有length()方法;数组有length属性
String类的常用方法
-
indexOf():返回指定字符串索引
-
charAt():返回指定索引处字符串
-
replace():字符串替换
-
trim():去除字符串两端空格
-
split():分割字符串,返回一个分割后的字符串数组
-
getBytes():返回字符串的byte类型数组
-
length():返回字符串长度
-
toLowerCase():将字符串转小写字母
-
toUpperCase():字符串字母转大写
-
substring():截取字符串
-
equals():字符串内容比较
stringbuilder、string、stringbuffer区别
可变性:
string不可变
stringbuffer、stringbuilder可变
安全性:
string、stringbuffer线程安全
stringbuilder线程不安全
性能:
对string进行改变时,会生成一个新对象,让后将指针指向新对象
stringbuffer每次会对对象本身进行操作
stringbuilder比stringbuffer性能好,但是线程不安全
使用总结:
少量数据:string
单线程操作字符缓冲区下操作大量数据:stringbuilder
多线程操作字符缓冲区下操作大量数据:stringbuffer
自动装箱与拆箱
装箱:将基本数据类型用对应的引用类型包装起来
拆箱:将包装类型转换为基础数据类型
原始类型:boolean 、char、byte、short、int、long、float、double
包装类型:Boolean、Character、Byte、Short、Integer、Long、Float、Double
集合框架定义
集合:用于存储数据的容器
集合框架:是为表示操作集合而规定的一种统一的标准的体系结构。任何集合框架都包含三大块内容:对外的接口、接口的实现、集合运算的算法
接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到多态。在面向对象编程语言中,接口通常用来形成规范
实现:集合接口的具体实现,是重用性很高的数据结构
算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序
算法是多态的
算法时可复用的函数
集合的特点
-
对象封装数据:集合用于存储对象
-
对象的个数确定可以使用数组、对象个数不确定可以使用集合。因为集合长度可变
集合与数组的区别
-
数组长度固定、集合长度可变
-
数组既可以存储基础数据类型也可以存储引用数据类型;集合只能存储引用数据类型
-
数据存储的元素必须是同一个数据类型;集合存储的数据类型可以是不同的数据类型
数据结构:容器中存储数据的方式
使用集合框架的优点
-
容量自增长
-
提供了高性能的数据结构和算法,编码更轻松,提高了程序速度和质量
-
允许不同的API之间的互操作,API之间可以来回传递集合
-
可以方便的扩展或改写集合,提高代码复用性和可操作性
-
使用JDK自带集合类,降低代码维护和学习新API成本
常用集合接口
集合:
-
单列集合collection
-
set
-
HashSet(无序、唯一):基于HashMap实现,底层使用JHashMap保存数据
-
TreeSet
-
LinkedHashSet(有序、唯一):红黑树:自平衡排序二叉树
-
-
list
-
ArryList(有序):Object数组
-
LinkedList(有序):双向循环链表
-
Stack
-
Vector:Object数组
-
-
-
双列集合map
-
HashMap:1.8之后,数组+链表+红黑树,链表和红黑树为了解决hash冲突,当链表长度大于阈值(默认8)时,链表变成红黑树
-
TreeMap:红黑树,自平衡排序二叉树
-
HashTable:数据+链表,链表解决hash冲突
-
ConcurrentHashMap
-
peoperties
-
LinkedHashMap
-
线程安全 的集合
-
vector:比arrylist多一个同步化机制,但是效率低,不建议使用
-
statck:堆栈类,先进后出
-
hashtable:比hashmap多个线程安全
-
enumeration:枚举,相当于迭代器
Java集合的快速失败机制“fail-fast”
是Java集合的错误检测机制,当多个线程对集合进行结构上的改变的操作时,可能产生fail-fast机制
例子: 线程一通过iterator遍历集合A中的元素,这是线程2修改集合A的结构,此时程序会抛出ConcurrentModificatuinException异常,从而产生fail-fast机制
原因:迭代器在遍历是直接访问呢集合中的内容,并且在遍历过程中使用了一个modCount变量。集合如果在遍历期间发生变化,就会改变modCount值。每当迭代器使用hasnext()/next()便利下一个元素之前,就会检测modCOunt变量是否为expectedmodCount值,true返回便利,否则抛出异常
解决方案:
-
遍历过程中设计modCount值改变的地方加上synachronized
-
使用CopyOnWriteArrayList代替Arrylist
怎么确保一个集合不被修改
使用Collections.unmodfiableCollection(Collection c)方法创建一个只读集合
collection<string> clist = Collection.ubnodifiableCollection(list);
Collection接口
List接口
迭代器Iterator
iterator接口提供遍历任何Collection的接口。
可以从一个Collection中使用迭代器方法来获取迭代器实例
迭代器取代了java框架中的Enumeration,
迭代器允许调用者在迭代过程中溢出元素
Iterator使用方法
List<String> list1 = new ArrayList<>(); list1.add(0,"123123"); list1.add(1,"123123"); list1.add(2,"123123"); list1.add(3,"123123"); Iterator<String> it = list1.iterator(); while(it.hasNext()){ String obj = it.next(); System.out.println("obj = " + obj); }
Iterator特点
-
单向遍历
-
安全,遍历的集合元素被改变时会抛出异常
如何边遍历一遍移除Collection中的元素
Iterator.romove()
ListIterator与Iterator的区别
ListIterator | Iterator |
---|---|
只能遍历list | 可以遍历set和list |
只能单向遍历 | 可以双向遍历 |
实现了Iterator接口,添加了一些额外功能 |
遍历List的几种方式
-
for循环遍历:基于计数器,在集合外部维护一个计数器,然后依次读取每一个位置的元素,读取到最后一个元素后停止
-
迭代器遍历:Iterator,Iterator是面向对象的一个设计模式,目的时屏蔽不同数据集合的特点,统一遍历集合接口。
-
foreach循环遍历:foreach内部采用了Iterator的方式实现,使用时不需要显示声明Iterator或计数器。
-
优点:代码简洁,不易出错
-
缺点:只能做简单的遍历,不能在遍历过程中操作数据集合
-
ArrayList的优缺点
优点:
-
查找速度很快,ArrayList底层以数组实现,是一种随机访问模式。实现了RandomAccess接口
-
在顺序添加一个元素时非常方便
缺点:
-
删除元素时需要做一次元素复制操作,比较耗费性能
-
插入元素时需要做一次复制操作
ArrayList适合顺序添加、随机访问的场景
数组和List之间相互转换
数组转List:Arrays.asList(attay)
List转数组:List.toArray()
ArrayList和LinkedList之间的区别:
区别 | ArrayList | LinkedList |
---|---|---|
数据结构实现 | 动态数组 | 双向循环链表 |
随机访问效率 | 高, | 低,线性存储结构,需要移动指针依次查找 |
增删效率 | 首位增删时高,其他地方增删时,会影响其他数据的下标 | 非首尾时,效率高 |
内存空间占用 | 少 | 多,除了存储数据好要存储两个引用,前置和后置元素的指针 |
线程安全 | 不安全 | 不安全 |
读取操作更多时,推荐使用arraylist
增删操作更多时:推荐Linkedlist
ArrayList与Vector对比
相同点:都是有序集合
不同点:
-
线程安全性:ArrayList非线程安全;Vector线程安全
-
性能:ArrayList性能更好
-
扩容:Vector每次扩容长度增加一倍,ArrayList增加50%
多线程场景下如何使用ArrayList
ArrayList不是线程安全的,可以使用Collections的synchornizedList方法将其转换成线程安全的容器
List<String> synchronizedList = Collections.synchnizedList(list);
List和set的区别
List:一个有序容器(元素存入集合的顺序和取出的顺序一致)、元素可以重复、可以插入多个null元素、元素都有索引、常用的实现类有:ArrayList、LinkedList、Vector
set:一个无序集合(存入和取出顺序可能不一致),不可以存储重复元素,只能存一个null元素、必须保证元素的唯一性、常用的实现类:HashSet、LinkesHashSet、TreeSet
List支持for循环,也就是通过下标遍历,也可以使用迭代器;set只能使用迭代,因为set无序,无法用下标获得想要的值
特点对比:
-
set:检索效率低,删除插入效率高,删除插入不会引起元素位置改变
-
List:查找效率高,插入删除元素效率低,会引起其他元素位置改变
set接口
HsahSet实现原理
HashSet是基于HashMap实现,HashSet的值存放在HashMap的key上,HashMap的value统一为PRESENT,因此HashSet的实现比较简单,相关操作都是直接调用HashMap的相关方法完成,HashSet不允许有重复的值
HashSet如何检查重复,如何保证数据不可重复
向HashSet中add()元素时,会使用hashmap的put()方法
HashMap的key是唯一的,在HashMap中如果K/V相同,会用新的K覆盖掉旧的K,让后返回旧的k
HashSet怎么存储hashCode相同,equals()为false的元素
将元素放在同样的哈希值下顺延(可以理解为放在一个哈希桶中)
TreeSet(二叉树)
-
使用二叉树的原理对新add()的对象按照指定的顺序排序(升序降序),每增加一个对象都会进行排序,将对象插入二叉树指定位置。
-
Integer和string对象都可以进行默认的TreeSet排序,而自定义的类的对象是不可以的,自定义的类必须实现Comparable接口,并复写相应的compare To()函数,才可以正常使用
-
在覆写compare()函数时,要返回相应的值才能使TreeSet按照一定规则排序
-
比较此对象与指定对象的顺序,此对象小于、等于、大于指定对象,则分别返回负整数、0、正整数
LinkHashSet
HashSet+LinkedHshMap
HashMap与HashSet的区别
HashMap | HashSet |
---|---|
实现map接口 | 实现set接口 |
存储键值对 | 存储对象 |
添加元素put() | 添加元素add() |
使用键(key)计算HashCode | 使用成员对象计算HashCode |
性能好 | 效率较慢 |
Queue
BlockingQueue定义
Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,他会等待队列变为非空;当添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口时Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要等待生成者有可用空间,或消费者有可用对象,因为它都在BlockingQueue的实现类中被处理了
Map接口
HashMap实现原理
概述:HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可实现的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是他不保证该顺序恒久不变
HashMap的数据结构:在Java编程语言中,基本数据结构就两种,一个是数组,另一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造。HashMap实际上是“链表散列”的数据结构,即数组和链表的结合体
Hash Map基于Hash算法实现
-
当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素数组中的下标
-
存储时,如果出现hash值相同的key,此时有两种情况
-
如果key相同,则覆盖原值
-
key不相同,则将当前的K/V放入链表中
-
-
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值
-
HashMap解决Hash冲突,核心就是使用了数组的存储方式,将冲突的key的对象放入链表中,一旦发生冲突就在链表中进一步对比
-
在Java8之后,当链表节点数据超过8个,链表就会转化为红黑树,时间复杂度从O(n)变成O(logn)
-
JDK1.7与JDK1.8对比
HashMap的put方法的具体流程
当我们执行put操作的时候,首先计算key的hash值,这里调用了hash方法,hash方法实际上是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以hash函数大概的左右就是:高16bit不变,低16bit和高
16bit做了一个异或。目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index= (table.length - 1)&hash,如果不做hash处理,相当于散列生效的只有几个低bit位,为了减少散列的碰撞,综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理碰撞,而且JDK1.8之后使用红黑树提升碰撞后的性能
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 } 4 5 static final int hash(Object key) { 6 int h; 7 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 8 } 9 10 //实现Map.put和相关方法 11 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 12 boolean evict) { 13 Node<K,V>[] tab; Node<K,V> p; int n, i; 14 // 步骤①:tab为空则创建 15 // table未初始化或者长度为0,进行扩容 16 if ((tab = table) == null || (n = tab.length) == 0) 17 n = (tab = resize()).length; 18 // 步骤②:计算index,并对null做处理 19 // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这 个结点是放在数组中) 20 if ((p = tab[i = (n - 1) & hash]) == null) 21 tab[i] = newNode(hash, key, value, null); 22 // 桶中已经存在元素 23 else { 24 Node<K,V> e; K k; 25 // 步骤③:节点key存在,直接覆盖value 26 // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 27 if (p.hash == hash && 28 ((k = p.key) == key || (key != null && key.equals(k)))) 29 // 将第一个元素赋值给e,用e来记录 30 e = p; 31 // 步骤④:判断该链为红黑树 32 // hash值不相等,即key不相等;为红黑树结点 33 // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可 能为null 34 else if (p instanceof TreeNode) 35 // 放入树中 36 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 37 // 步骤⑤:该链为链表 38 // 为链表结点 39 else { 40 // 在链表最末插入结点 41 for (int binCount = 0; ; ++binCount) { 42 // 到达链表的尾部 43 44 //判断该链表尾部指针是不是空的 45 if ((e = p.next) == null) { 46 // 在尾部插入新结点 47 p.next = newNode(hash, key, value, null); 48 //判断链表的长度是否达到转化红黑树的临界值,临界值为8 49 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 50 //链表结构转树形结构 51 treeifyBin(tab, hash); 52 // 跳出循环 53 break; 54 } 55 // 判断链表中结点的key值与插入的元素的key值是否相等 56 if (e.hash == hash && 57 ((k = e.key) == key || (key != null && key.equals(k)))) 58 // 相等,跳出循环 59 break; 60 // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 61 p = e; 62 } 63 } 64 //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的val ue这个值 65 if (e != null) { 66 // 记录e的value 67 V oldValue = e.value; 68 // onlyIfAbsent为false或者旧值为null 69 if (!onlyIfAbsent || oldValue == null) 70 //用新值替换旧值 71 e.value = value; 72 // 访问后回调 73 afterNodeAccess(e); 74 // 返回旧值 75 return oldValue; 76 } 77 } 78 // 结构性修改 79 ++modCount; 80 // 步骤⑥:超过最大容量就扩容 81 // 实际大小大于阈值则扩容 82 if (++size > threshold) 83 resize(); 84 // 插入后回调 85 afterNodeInsertion(evict); 86 return null; 87 }
-
判断键值对数组table[i]是否为空或null
-
根据键值key计算hash值的到插入数组索引i,如果table[i]==null,直接新建节点添加,转向步骤6,若table[i]不为空,转向3
-
判断table[i]的首个元素和key是否一样,如果相同直接覆盖value,否则转向4,这里的相同值hashcode和equals
-
判断table[i]是否为treeNote,即table[i]是否时红黑树,如果是,则直接在树中插入键值对,否则转向5
-
遍历table[i],判断链表长度是否大于8,大于8的话将链表转为红黑树,在红黑树中执行插入操作,否则进行链表插入操作,遍历过程中发现key已存在则直接覆盖value
-
插入成功,判断实际存在的键值对数量size是否超过最大容量threshold,超过则进行扩容
HashMap扩容操作怎么实现
-
在jdk1.8中,resize方法是在hashmap中的键值对大于阈值时或初始化时,就调用resize方法进行扩容
-
每次扩容都是扩展两倍
-
扩容后Nod对象要们在原位置,要么移到原偏移量2倍的位置。重新进行hash分配之后,该元素要门在原位置,要么移动到原始位置+增加的数组大小 这个位置
1 final Node<K,V>[] resize() { 2 Node<K,V>[] oldTab = table;//oldTab指向hash桶数组 3 int oldCap = (oldTab == null) ? 0 : oldTab.length; 4 int oldThr = threshold; 5 int newCap, newThr = 0; 6 if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空 7 if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀 值 8 threshold = Integer.MAX_VALUE; 9 return oldTab;//返回 10 }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16 11 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 12 oldCap >= DEFAULT_INITIAL_CAPACITY) 13 newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold 14 } 15 // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初 始化成最小2的n次幂 16 // 直接将该值赋给新的容量 17 else if (oldThr > 0) // initial capacity was placed in threshold 18 newCap = oldThr; 19 // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75 20 else { // zero initial threshold signifies using defaults 21 newCap = DEFAULT_INITIAL_CAPACITY; 22 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 23 } 24 // 新的threshold = 新的cap * 0.75 25 if (newThr == 0) { 26 float ft = (float)newCap * loadFactor; 27 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 28 (int)ft : Integer.MAX_VALUE); 29 } 30 threshold = newThr; 31 // 计算出新的数组长度后赋给当前成员变量table 32 @SuppressWarnings({"rawtypes","unchecked"}) 33 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组 34 table = newTab;//将新数组的值复制给旧的hash桶数组 35 // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素 重排逻辑,使其均匀的分散 36 if (oldTab != null) { 37 // 遍历新数组的所有桶下标 38 for (int j = 0; j < oldCap; ++j) { 39 Node<K,V> e; 40 if ((e = oldTab[j]) != null) { 41 // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收 42 oldTab[j] = null; 43 // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树 44 if (e.next == null) 45 // 用同样的hash映射算法把该元素加入新的数组 46 newTab[e.hash & (newCap - 1)] = e; 47 // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排 48 else if (e instanceof TreeNode) 49 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 50 // e是链表的头并且e.next!=null,那么处理链表中元素重排 51 else { // preserve order 52 // loHead,loTail 代表扩容后不用变换下标,见注1 53 Node<K,V> loHead = null, loTail = null; 54 // hiHead,hiTail 代表扩容后变换下标,见注1 55 Node<K,V> hiHead = null, hiTail = null; 56 Node<K,V> next; 57 // 遍历链表 58 do { 59 next = e.next; 60 if ((e.hash & oldCap) == 0) { 61 if (loTail == null) 62 // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead 63 // 代表下标保持不变的链表的头元素 64 loHead = e; 65 else 66 // loTail.next指向当前e 67 loTail.next = e; 68 // loTail指向当前的元素e 69 // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素 时, 70 // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next..... 71 // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。 72 loTail = e; 73 } 74 else { 75 if (hiTail == null) 76 // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素 77 hiHead = e; 78 else 79 hiTail.next = e; 80 hiTail = e; 81 } HashMap是怎么解决哈希冲突的? 答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什 么是哈希才行;什么是哈希? Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成 固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通 常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入 值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。 所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也 不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。 什么是哈希冲突? 当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰 撞)。 HashMap的数据结构 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除 困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各 自的优势,使用一种叫做链地址法的方式可以解决哈希冲突: 82 } while ((e = next) != null); 83 // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。 84 if (loTail != null) { 85 loTail.next = null; 86 newTab[j] = loHead; 87 } 88 if (hiTail != null) { 89 hiTail.next = null; 90 newTab[j + oldCap] = hiHead; 91 } 92 } 93 } 94 } 95 } 96 return newTab; 97 }
HashMap是怎么解决哈希冲突
哈希冲突 也叫 散列冲突
散列函数基本特征:根据同一散列函数计算的散列值如果不同,那么输入值肯定也不同,但是如果根据同一散列函数计算出的散列值如果相同,输入值不一定相同
哈希冲突:当输入两个不相同的值,根据同一散列函数计算出相同的散列值
使用链地址法,可以解决哈希冲突:将哈希值相同的对象组织成一个链表放在hash值所对应的bucket下,由于hashmap的初始容量远小于int类型的范围,如果只是单纯的使用hashcode函数取余来获取对应的bucket,会大大增加hash碰撞的概率,所以要想办法让hashcode取出的高位参与运算,该操作称为扰动
JDK1.8的hash函数
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位 进行异或运算(高低位异或) 4 }
进行1次位运算,1次异或运算(2次扰动)
总结:
-
使用链地址法链接有相同hash值的数据
-
使用2次扰动降低hash冲突概率
-
使用红黑树进一步降低遍历的时间复杂度
红黑树删除、添加节点
能否使用任何类作为Map的key
可以使用任意类
需要考虑一下几点:
-
类如果重写了equals方法,也应该重写hashCode方法
-
如果一个类没使用equals方法,则不应该在hashcode中使用
HashMap中string、integer等包装类为什么适合做key
-
final类型,保证key的不可更改行
-
重写了equals()、hashcode()方法,遵守内部规范,不易出现hash值计算错误
要使用Object作为HashMap的key
-
重写hashCode()方法和equals()方法
HashMap为什么不直接使用hashCode()处理后的hash值作为table的下标
hashCode()方法返回的值范围太大,远超HashMap的存储范围,HashMap一般取不到hashCode()返回的大值
HashMap长度为什么是2的幂
为了让HashMap存取高效,尽量减少碰撞
HashMap与HashTable的区别
-
HashMap非线程安全;HashTable线程安全
-
HashMap效率高;HashTable效率低
-
对Null Key /null value的支持:hashmap只能有一个null键,可以有多个null值;HashTable不能有空键值,put null时会报空指针异常
-
初始容量大小与每次扩容大小:
-
Hashtable默认初始大小为11,若给定容量初始值,则直接使用给定的大小,扩容容量变为原来的2n+1
-
HashMap默认初始大小为16,扩容则变为原来的2倍
-
-
底层数据结构:HashMap:数组+链表+红黑树;HashTable:数组+链表、
HashMap与TreeMap选择
-
HashMap适合不需要排序,只需要快速访问和存储键值
-
TreeMap适用于需要按键顺序遍历元素的场景
Tree Map添加节点
TreeMap删除节点
HashMap与ConcurrentHashMap的区别
区别 | HashMap | ConcurrentHashMap |
---|---|---|
线程安全性 | 不安全 | 安全 |
底层数据结构 | 数组、链表、红黑树 | 数组、链表、红黑树;1.8之前是Segment数组、链表、红黑树 |
锁机制 | 无内置锁 | 使用分段锁(jdk1.8之前)或CAS(1.8以后) |
性能特点 | 不涉及同步操作,性能高 | 并发性能更好 |
迭代器行为 | 迭代过程中修改会抛异常 | 迭代过程中可以修改 |
空键空值 | 允许一个null键、多个null值 | 不能有null键,可以有多个null值 |
扩容机制 | 元素数量到阈值后使用rehash扩容2倍 | 类似HashMap,要通过更复杂的机制确保在并发环境下扩容 |
使用场景 | 单线程、读多写少 | 多线程并发读写 |
HashMap死循环
HashMap并发时可能死循环,导致cpu100%
原因分析:HashMap使用一个Entry数组保存key、value数据,当一对key、value被加入时,会通过hash算法得到数组的下标index,具体算法:根据key的hash值,对数组的大小取模hash&(length-1),并把结果插入数组该位置,如果该位置已有元素,则说明存在hash冲突,则在该位置生成链表
在并发情况下,发生扩容时,可能会产生循环链表,在执行get方法时会触发死循环
为何没解决:Sun公司认为HashMap本就不支持多线程使用
ConcurrentHashMap实现线程安全的方法
JDK1.7
-
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段 数据时,其他段的数据也能被其他线程访问。
-
在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:
-
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一 种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构 的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修 改时,必须首先获得对应的 Segment的锁。
-
JDK1.8
-
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
-
结构如下:
-
辅助工具类
comparable与comparator的区别
-
comparable接口出自java.lang包,有一个compareTo(Object obj)方法排序
-
comparator接口出自java.util包,有一个comparator(Object obj1,Object obj2)方法用来排序
-
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().
Collection 和 Collections的区别
-
java.util.Collection是一个集合接口。提供了对几个对象进行基本操作的通用接口方法。Collection接口的意义是为各种具体夫人集合提供最大化的统一操作方式
-
Collections是集合类的一个工具类,提供一系列静态方法,用于对集合中的元素进行排序、搜索、线程安全等操作
TreeMap和TreeSet在排序是如何比较元素?Collections工具类中的sort()方法如何比较元素?
-
TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素compareTo()方法,当插入元素时会回调该方法比较元素大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序
-
Collections工具类的sort()方法有两种重载形式
-
要求传入的待排序的容器中存放的对象比较实现Comparable接口以实现元素的比较
-
不强制性要求容器中的元素必须可以比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compara方法实现元素的比较),相当于临时定义的排序规则,其实就是通过接口注入比较元素大小,也是对回调模式的应用(Java中对函数式编程的支持)
-
HashMap扩容因子
默认0.75
Java中的集合类
-
list
-
set
-
queue
-
map
java中的队列
-
ArrayDeeque(数组双端队列)
-
PriorityQueue(优先级队列)
-
ConcurrentLikedQueue(基于链表的并发队列)
-
DelayQueue(延期阻塞队列)
-
ArrayBlockingQUeue(基于数组的并发阻塞队列)
-
LinkedBLockedQueue(基于链表的FIFO阻塞队列)
-
LinkedBlockingDeque(基于链表的FIFO双端阻塞队列)
-
PriorityBlockingQueue(带优先级的无界阻塞队列)
-
SynchronousQueue(并发同步阻塞队列)
反射中Class.forName和classloader的区别
-
class.forName()除了将类的.class文件加载到jvm中,还会对类进行解释,执行类中的static块
-
class.forName()可以用参数控制是否加载static代码块
-
classLoader只是将.class文件加载到jvm中,不会执行static中的内容,只有调用newInstance()方法才会执行static块
IO流的概念
流是一组有顺序、有起点有重点的字节集合,是对数据传输的总称或抽象。即数据在两设备之间传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作
流有输入和输出,输入时是从数据源流向程序。输出时是从程序流向数据源。数据源可以是内存、文件、网络或程序
字节流和字符流
用法基本一样,主要区别在于操作的数据单元不同
字符流由于数据编码的不同,而有了对字符高效操作的流对象。本质就是基于字节流读取时去查了指定的码表
区别:
-
读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可以读取多个字节
-
处理对象不同:字节流能处理所有类型数据(图片、avi),字符流只能处理字符类型数据
除了处理纯文本数据,优先考虑字符流,除此之外都是用字节流
字节流和处理流
按照流的角色分:
-
节点流:可以从一个特定的IO设备读写数据的流,也叫低级流
-
处理流:对一个已经存在的流进行连接或封装,通过封装之后的流实现数据读写,也较高级流
当使用节点流进行输入输出时,程序并不会直接连接到实际的数据源。
好处:只要使用相同的处理流,程序就可以使用完全相同的输入输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序所实际访问的数据源也相应 的变化。
使用处理流来包装节点流是一种典型的装饰器设计模式,通过处理流包装不同的节点流,可以消除不同节点流的实际差异,可以提供更方便的方法完成输入输出功能
IO流框架图
IO模型分类
按照《Unix网络编程》
-
阻塞IO
-
非阻塞IO
-
IO复用模型
-
信号驱使式IO
-
异步IO
按照POSIX
-
同步IO
-
异步IO
一个IO操作有两个步骤:发起IO请求、实际IO操作
区分同步IO、异步IO
同步:实际的IO读写阻塞请求进程
异步:IO读写不阻塞请求进程
-
同步与异步关注的是消息通信机制。所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值,就是调用者主动等待这个调用的结果
阻塞与非阻塞
-
阻塞与非阻塞关注的是程序等待调用结果(消息、返回值)时的状态,阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回;而非阻塞调用指在不能立刻获得结果之前,该调用不会阻挡当前线程
两种io多路复用方案
IO多路复用机制都依赖于一个事件多路分离器。分离器对象可将事件源的io时间分离出来,并分发到对应的read/write事件处理器。开发人员预先注册需要处理的事件以及事件处理器;时间分离器负责将请求事件传递给事件处理器。
两个与事件分离器有关的模式时Reactor和Proactor。Reactor采用同步IO,而Proactor采用异步IO。在Reactor中,时间分离器负责等待文件描述或socket为读写操作准备就绪,让后将就绪事件传递给对应处理器,最后由处理器完成实际的读写工作
Java中的异常
Java中,所有异常的共同祖先java.lang包中的Throwable类,Throwable下面有两个重要的子类:Exception(异常),Error(错误),二者都是Java异常处理的重要子类,各自都包含大量子类
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重的问题。大多错误与代码编写者执行的操作无关,表示代码运行时JVM出现的问题
Exception(异常):是程序本身可以处理的异常
-
RuntimeException运行时异常
-
NullPointerException 空指针异常
-
ArithmeticException 算术运算异常
-
ArrayIndexOutOfBoundSException下标越界异常
异常处理总结
-
try块,用于捕获异常,后面可以借0个或多个catch块,没有catch则必须有一个finally
-
catch:处理try捕获到的异常
-
finally:无论是否捕获到异常,finally中的语句都会被执行
finally块不会执行的特殊情况
-
finally语句块中出现异常
-
前面的代码使用system.exit()退出程序
-
程序所有线程死亡
-
关闭cpu
Java序列化中有些字段不想进行序列化
对于不想进行序列化的变量,使用transient关键字修饰
transient关键字的作用时:阻止实例中那些用此关键字的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法
获取键盘输入
方法一Scanner Scanner input = new Scanner(System.in); String s = input.nextLine(); input.close(); 方法二 BufferedReader BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); String s = input.readLine();
Java注解(Annotation)
概念:对元程序中元素关联信息和元数据的途径和方法,
元注解
-
@Target :说明了Annotation所修饰对象的范围
-
@Retention:定义被保留的时间长短
-
@Documented:标记一个类的对象应该被持久化存储
-
@Inherited:指定一个注解是否可以被继承,当一个类生成子类时,如果父类使用了带有@Inherited的注解,那么这个注解也会被子类继承
注解处理器
在编译阶段自动处理源码中的注解,并根据这些注解生成额外的代码或资源文件
Java泛型
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型方法():
你可以写一个泛型方法,该方法可以接受不同类型的参数。根据传递给泛型方法的参数类型,编译器适当的吃力每一个方法调用
//泛型方法 public static <E> void printArray(E[] inputArray){ for (E element : inputArray){ system.out.printf("%s",element); } }
-
<?extends T>表示该通配符所代表的类型是T类型的子类
-
<?super T> 表示该通配符所代表的类型是T类型的父类
泛型类
泛型类的声明和非泛型类的声明相似,除了在类名后添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个参数类型,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类名称的标识符。因此它们接受一个或多个参数,这些类被称为参数化的类型或参数化的类
public class Box<T>{ private T t; public void add(T t){ this.t = t; } public void get(T t){ return t; } }
类型通配符
类型通配符一般是使用?代替具体参数类型。例如List<?>在逻辑上是List等所有List<具体参数类型>的父类
类型擦除
Java中的所有泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中不包含泛型中的类型信息的。使用泛型的时候加上类型参数,会被编译器在编译的时候去掉。这个过程就是类型擦除。在代码中定义的list等类型,在编译之后都会变成list。JVM看到的只是List,由泛型附加的类型信息对JVM是不可见的
类型擦除的基本过程:找到用来替换类型参数的具体类。一般是Object,如果指定了类型参数的上界的话,则使用上界。把代码中的类型参数换成具体的类。
序列化
Java序列化(创建可复用的Java对象)
含义:保存(持久化)对象及其状态到内存或者磁盘
对象的生命周期不会比JVM生命周期长,但是在现实中,可能要求JVM停止之后能保存指定的对象,并能重新读取被保存的对象
序列化对象以字节数组保存-静态成员不保存
使用Java对象序列化,在保存对象时,会把其状态保存为一节数组,在未来,再将这些字节组装成对象。对象序列化,保存的是对象的“状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
序列化远程对象传输
使用RMI(远程方法调用),或者在网络中传递对象,都会使用到对象序列化,
序列化实现方法
Serializable实现序列化
只要一个类实现java.io.Serizable接口,那么它就可以被序列化。
ObjectOutPutStream和ObjectInputStream对对象进行序列化以及反序列化
在类中添加writeObject和readObject方法可以实现自定义序列化策略
序列化ID
虚拟机是否支持反序列化,不仅取决于类路径和功能代码是否一致,还有就是两个类序列化ID是否一致:就是:private static final serialVersionUID
序列化子类,想要将父类对象也序列化,就需要让父类也实现Serializable接口
Transient关键字阻止该变量被序列化到文件中
-
在变量声明前加上Transient关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如int是0,对象型是null
-
服务端给客户端发送序列化对象数据,对象中有一些数据是敏感的,希望对该密码字段在序列化时,进行加密,如果客户端有解密密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度上保证序列化对象数据安全
Java复制
将一个对象的引用复制给另一个对象,有三种方式①直接赋值②浅拷贝③深拷贝
直接赋值复制
在Java中a1=a2,复制的是引用,a1,a2指向的是同一个对象,当a1改变时,a2也会改变,
浅复制(复制引用但不复制引用的对象)
创建一个新对象,将当前对象的非静态字段复制到新对象;如果字段是值类型,对该字段执行复制;如果是引用类型,则复制引用但不复制引用的对象
深复制(复制对象及其应用对象)
不仅复制对象本身,而且复制对象包含的引用指向的所有对象
浅拷贝和深拷贝都需要继承Cloneable,重写clone()方法
序列化
在Java语言里面深复制一个对象,常常可以先使对象实现Serializable接口,让后把对象(对象的一个拷贝),写道一个流里面,再从流里面读出来,便可以重建对象
Java9比Java8改进了什么
-
引入了模块系统,采用模块化系统的应用程序只需要这些应用程序所需的那部分JDK模块,而非整个JDK框架,减少了内存开销
-
引入了新的package:java.net.http,里面提供了对Http访问很好的支持,不仅支持Http1.1,还支持Http2
-
引入jshell这个交互性工具,让Java可以像脚本语言一样运行,可以从控制台启动jshell,在jshell直接输入表达式并查看执行结果,
-
增加了List.of(),Set.of(), Map.of() , Map.ofEntries(),等工厂方法来创建不可变集合
-
HTML5风格的Java帮助文档
-
多版本兼容JAR功能,能让你创建仅在特定版本的Java环境中运行库程序时,选择使用的class版本
-
统一JVM日志,可以使用新的命令行选择-Xlog来控制JVM上所有组件的日志记录。该日志记录系统可以设置输出日志的标签、级别、修饰符和输出目的
-
垃圾收集机制:Java9移除了在Java8中废弃的垃圾回收器配置组合,同时把G1设为默认的垃圾回收器实现,相对于Parallel来说,G1会在应用线程上做更多事,而Parallel几乎没有在引用程序上做任何事,基本上完全依赖GC线程完成所有的内存管理,这意味着切换到G1会为线程带来额外的工作,直接影响到应用性能
-
I/O新特新: java.io.InputStream 中增加了新的方法来读取和复制 InputStream 中包含的数据。 readAllBytes:读取 InputStream 中的所有剩余字节。 readNBytes: 从 InputStream 中读取指定数量 的字节到数组中。 transferTo:读取 InputStream 中的全部字节并写入到指定的 OutputStream 中 。
Java8 十大特性
1.接口的默认方法
接口可以定义一个非抽象的实现方法,要使用default关键字,实现了该接口的子类,只需要实现其中一个方法,就可以直接使用默认方法
2.Lambda表达式
排列字符串
List names = Arrays.asList("asd","dfger","sf") //给静态方法Collections.sort传入一个List对象以及一个比较器来指定顺序排列。一般是创建一个匿名的比较器对象,让后将其传递给sort方法 Collections.sort(names,new Comparator(){ @Override public int compareTo(String a,String b){ return b.compareTo(a); } }); //java8中的lambda表达式写法 Collections.sort(name,(String a,String b)-> return b.compareTo(a));
3.函数式接口
仅仅只包含一个抽象方法的接口
4.方法与构造函数引用
使用::关键字传递方法或构造函数引用
5.lambda作用域
lambda表达式访问外层作用域:可直接访问标记了final的外层局部变量,或者实例的字段以及静态变量
6.访问局部变量
可直接在lambda表带是中访问外层局部变量
7.lambda访问对象字段与静态变量
8.lambda访问接口的默认方法
9.Data API
10.Annotation 注解
Java9新特性
模块系统
重点:HashMap底层原理:红黑树旋转(左旋、右旋),扩容、hash算法、
concurrentHashMap、结构
Hash冲突解决方案
-
开放地址法:发生冲突时,按照一定规则修改索引值,再次尝试,知道找到空位置,常见方法:线性探测、二次探测、随机探测
-
链式地址法:使用链表存储所有散列到该桶的记录
-
建立公共溢出区:建立一个用于存放所有冲突元素的公共区域,基本表中不含冲突元素,发生冲突的元素依次放入溢出表中
-
再哈希法:建立不同的哈希函数,当第一个哈希函数导致冲突时,就使用第二个哈希函数,以此类推,直至没有哈希冲突
java多态原理
运行时多态,编译时多态
编译时多态:方法重载
运行时多态:动态方法绑定
实现原理:运行时类型识别、方法表、Java虚拟机(JVM)内存结构;
当代码在JVM上运行时,每个类都会在方法区中拥有内存信息,包括一个方法表,用于确定哪些方法是该类所拥有的。子类可以通过重写来提供父类方法的具体实现。当调用方法时,JVM会查找相应对象的实际类型,并调用正确的方法版本
继承、封装、多态
-
继承
-
允许一个类继承另一个类的属性和方法,从而复用代码,并建立类之间的层次关系
-
-
封装
-
将对象的属性和方法隐藏起来,并通过公开的方法(getter和setter)来访问和修改属性的机制
-
保证对象对象内部状态不被外部任意修改
-
-
多态
-
同一方法在不同对象中的不同实现
-
提高代码灵活性和可扩展性,时程序在运行时动态选择合适的方法执行
-
Object类的常见方法以及使用场景
-
equals
-
hashcode()
-
toString()
-
clone()
-
复制对象
-
-
finalize()
-
一般在垃圾回收器清理对象之前调用,通常用于清理资源;关闭文件、释放网络连接
-
-
getclass()
-
返回对象运行时的类信息,是‘Class<?>’类型对象,一般运用在需要反射操作时
-
-
notify()与notifyAll()
-
唤醒在该对象上等待的单个线程或所有线程(被wai()阻塞)
-
-
wait()
-
让当前线程等待,直到另一个线程调用notify()或notifyAll()方法,或者等待超时
-
Bean生命周期
创建、初始化、调用销毁
-
bean的创建有4种方式
-
构造器
-
静态工厂
-
实例工厂
-
setter注入
-
-
spring在调用对象的时候,由于bean作用域的不同,创建和初始化的时间也不相同,
-
作用域为singleton时,bean随着容器一起被创建和初始化,
-
作用域为pritotype,bean是在被调用时才创建和初始化。
-
-
初始化和销毁 可以干涉,可以自定义初始化和销毁方法
-
@postconstruct,自定义方法前加上该注解,spring容器会在调用完setBeanFactory方法后调用该方法
-
@preDestory注解,spring在自身销毁之前调用该方法
-
JWT具体的实现流程(spring boot整合)
-
添加依赖(jwt和springsecurity)
-
配置spring security,重写config方法配置安全策略
-
创建JWT工具类:负责生成和解析JWT令牌
-
创建JWT过滤器:从请求中获取JWT令牌,验证有效性,并将认证信息添加到springsecurity上下文
-
创建用户认证服务
-
创建控制器,处理用户登录、生成token令牌
添加依赖 创建JWT工具类,生成和解析JWT令牌 创建JWT拦截器,从请求头中提取JWT 配置安全策略,添加JWT拦截器 创建用户认证服务 创建认证控制器:处理请求、生成JWT