本文章为个人学习时所做笔记,有所欠缺多多包涵。
学前疑惑:
向上转型和向下转型不知道!
答:向上转型是子类实例赋值给父类类型,
向下转型是父类实例赋值给子类类型,需要加 '强转括号'。
为什么类只能单继承?
答:如果某个类继承多个类,多个类中有同名同参的方法,那么在执行过程中就会不知道该调用哪个方法,产生冲突。
什么是抽象类?抽象类和接口的区别?
答:从具有多个相同属性,相似方法的类中,抽取出来的由属性和方法组成的类叫做抽象类。
区别:接口是对行为的抽象,是抽象的集合。接口不能实例化,不能包含非常量成员,(public static final将变量修饰成常量);抽象类也不能实例化,抽象类的主要目的是提高代码重用。
为什么需要内部类?什么是匿名内部类?(熟悉)
答:内部类是实现多继承的方法之一;内部类可以很好的实现信息隐藏。
匿名内部类是实现接口和重写方法的手段,节省了代码量。不用创建类来实现接口和方法。
静态内部类和非静态内部类有什么区别?(了解)
答:静态内部类用static修饰,可以访问所有静态成员,包括私有,但是不能访问非静态的。
非静态内部类要想访问静态内部类必须先创建静态对象,再进行访问。
super的作用是什么,为什么能调用父类的构造方法?
调用父类的构造方法;
类里面存放的是属性和方法
封装:
封装:隐藏类的内部信息(如women类的age),对外提供统一接口。
封装的优点:隐藏实现细节/使代码便于修改/减少耦合
继承(this,super重写):
使子类具有获取父类的属性和方法的能力
- 继承的作用:
- 减少代码的使用量,使代码结构更清晰
三种实现多继承的方法:内部类、多层继承(c继承b,b又继承a)、实现接口
this和super关键字:
- this是指向自己的引用
this.属性 表示调用本类成员变量
this.方法() 表示调用本类的某个方法
this() 表示调用本类的构造方法(与类同名的方法,这也是构造方法不能继承的原因)
this和super的两种用法:以this举例
1.this用来区分本类中相同的属性名(分成员变量和构造方法中的局部变量)this.name成员=name局部 2.this(name)用来调用形参为name的构造方法。
- super是指向父类的引用
super.属性 调用父类的成员变量
super.方法() 调用父类方法
super() 调用父类构造方法
如果子类不写super,则默认调用父类的无参构造
当需要在子类中调用重写的父类方法时,需要用super.方法( )来调用方法
重写和重载:
重写Override:子类的方法与父类的方法的(方法名,返回值,参数类型、参数个数、参数顺序)统统相同,只是核心内容有变。*重写时建议加上@Override
重载Overload:同一个类中,定义多个同名方法,但是参数不同。
Object类和转型(==与equals):
object中没有成员变量,所以object类的构造方法是无参的,这也使得object能够成为顶级父类
object的成员方法:
toString方法:
是object的成员方法,返回的是地址值。如果想要看到对象的属性值,需要重写toString方法,返回属性值+'' ''
equals方法:
==:
基本数据类型不能调用equals方法,因为基本数据类型不是类,不是对象。所有基本数据类型只能通过==来比较,比较的是具体值。引用数据类型用==来比较时比较的是地址值。
equals:
首先,equals不能用于比较基本数据类型。如果没有重写equals方法,equals和==作用相同;如果重写了equals方法,则比较两个对象是否内容相同。
hashcode方法:
获取一个哈希码,返回对应的int整数,找到对象存放在哈希表中的位置,方法返回一个内存地址。
阿里巴巴的规矩:
浮点数之间的等值判断,基本类型不用==,包装类不用equals
向上转型向下转型:
向上转型(自动类型转换):将子类对象赋值给父类类型。(父类引用指向子类的对象)
Father father=new Son( );
当使用多态方式调用方法时,引用名称father只能访问父类中的属性和方法。编译器首先检查父类中有没有该方法,如果没有则编译错误;如果有,再去调用子类的同名方法。
将参数声明为父类类型,传参时传的是子类对象(默认发生向上转型)
*is-a原则:子类对象能够替代父类对象被使用
向下转型(强制类型转换):将父类对象实例赋值给子类类型。但父类的引用变量必须是子类对象才算转型成功。
使用多态时,想要访问子类特有的属性和方法时,必须向下转型。
使用向下转型时,最好用instance of关键字判断一个对象是否属于''指定类''或者''指定类的子类''的对象。
判断方法:等号右边有强转括号
如 Integer integer =(Integer) object;
多态:
同一个事件发生在不同对象上产生的不同结果。
实现多态的前提:
- 要有继承关系
- 子类要重写父类的方法
- 父类引用指向子类对象
多态的好处:
- 降低代码之间的耦合度
- 减少冗余代码
- 使得项目的扩展能力更强
修饰符
访问权限修饰符:public,privata,protected
子类重写继承的方法时,父类的'私密性''要比子类更高或者相同才行。
(抛出异常时,子类的异常范围也不能大于父类)
非访问修饰符:static,final,abstract
static:如果某个变量或者方法被static修饰了,那就不需要创建对象来对他进行访问,比如Math里的静态方法;
构造方法不能用static修饰,
static
静态变量:
静态变量又叫类变量,通常使用''类名.静态变量''来获取静态变量的值,也可以创建对象后通过实例(对象)来访问。
MyClass.staticVariable = 42; // 通过类名访问静态变量
int value = MyClass.staticVariable; // 通过类名获取静态变量的值
MyClass myObject = new MyClass();
myObject.staticVariable = 10; // 也可以通过对象实例访问静态变量
静态方法:
又称类方法,用static修饰,静态方法直接属于类,而不属于类的实例(实例和对象基本上是同义词,它们常常可以互换使用。对象代表了类的一个特定的实例。)
实例方法:
实例方法区别于静态方法,没有static修饰。只要不是在实例方法里面,就必须先创建''实例方法所在类''的对象,才能调用该实例方法。
静态方法和实例方法的区别:
final
final变量不能重新赋值,final和static一起使用可创建类常量
final方法可以继承,但不能重写
final类不能被继承
abstract
抽象方法:
有很多不同类的方法相似,但内容有所不同,我们可以抽取他们的声明,但是没有方法体。(即抽象方法可以表达概念但无法具体实现)
抽象类:
abstract class AbstractPlayer {
} 抽象类不能被实例化,但是可以创建子类来继承抽象类
public class BasketballPlayer extends AbstractPlayer {
}抽象类中应该至少有一个抽象方法
抽象类中的抽象方法没有方法体,子类必须去编写、实现
接口:
如果子类是非抽象类,则必须实现接口中的所有方法;
如果子类是抽象类,则可以不实现接口中的所有方法,因为抽象类中允许有抽象方法的存在!
- 接口:interface
- 接口的作用:
- 使某些实现类具有我们想要的功能
- 实现多态
- 通过接口可以实现多继承,比如让猪同时拥有跑(继承自classA)和飞(继承自classB)的能力
接口中的变量会在编译时自动加上public static final,变成常量,所以接口的变量值无法改变
jdk8后,接口中可以写静态方法,通过接口名来调用
jdk8后,接口中有默认方法default,既然要默认实现,就要有方法体,不能直接在()后用;结束
接口不允许直接实例化
实现接口必须重写里面的抽象方法
接口里面的方法都是抽象的,用abstra修饰
接口可以是空的,(标记型接口)如:只要实现了serializable接口就能进行序列化
接口是为了去让子类实现的,定义接口时不能用final;同理在写接口方法时,不应用private,protect,final修饰
构造方法:
- 构造方法要和类名相同
- 构造方法是为了初始化对象
内部类(了解):
内部类的特征:
内部类为多重继承提供了解决方案
内部类只能被外部类访问,封装更好
外部类想要访问内部类的成员,就必须创建一个成员内部类的对象来进行访问
局部变量是写在方法中的,方法结束后,局部变量也就消失了
局部内部类也是定义在方法或者作用域中的,生命周期仅限于作用域
匿名内部类: 当我们要实现一个接口时,需要创建一个类来继承接口,并且书写接口里面的方法具体怎么实现,但是我们不想创建,于是就有了匿名内部类,让我们不需要创建新的类。
主要用来继承其他类或者实现接口,对继承的方法进行实现或者重写?????
静态内部类 静态内部类不依赖外部类的实例,可以直接访问外部类的静态成员
面向对象特征(结束)
Java语言特征
形参:
定义方法时使用的参数,用于接收调用者传递的参数。
实参:
将值传递给方法,需要提前赋值。
值传递:
在调用方法时,将实际参数的值拷贝一份,传递给方法,这样不会修改实际参数的值。
引用传递:
在Java中,引用传递实际上不是传递的该引用本身,而是传递的该引用的一个副本,如果不对副本指向进行修改,那么这个副本和引用值就是同一个地址,操作副本就相当于操作引用,这里就和引用传递一致了。但是如对该副本修改了指向,那么修改的只是副本值,而不会对引用本身造成影响。
总结:
java中不管传递的是基本数据类型(原始数据类型int,double,char;包装类integer,character)还是引用数据类型,采用的都是值传递。string不是包装类!
final.finally.finalize区别:
final见目录:修饰符;
finally是保证重点代码一定要执行的,如try catch finally格式;
finalize是object的方法为了保证对象在被垃圾收集前,完成特定资源的回收。jdk9后被标记为deprecated。
static
见修饰符final
数据类型
学完后自答:
- Java基本数据类型有几种?各占多少位?
基本数据类型有八种,byte,short,int,long,char,double,float,boolean各自占8,16,32,64,16,32,64,8位
- 基础类型和包装类有什么区别?
基础类的效率高,因为基础类直接在栈中存放具体数值,不需要新建对象来获取地址值
但是基础类型不能作为对象引用方法,不符合java万物皆对象的特征。
包装类部分数据类型的内部有缓存机制,如short.int,long,byte的缓存范围都是-127~128
包装类可以有null值,而基本类型不可以,在某些数据库查询等操作中,包装类更加适用
包装类可以用来指定泛型,而基本类型不可以,List<Integer>list=new ArrayList<>( );
用==判断包装类时,如果数据在缓存范围当中,将不会创建对象,而是直接用缓存中的对象,当有另一个相同值的数据时,两个包装类将指向同一位置
- 自动装箱与拆箱了解吗?原理是什么?
jdk5以后将会进行自动装箱拆箱。原理是通过valueOf方法,将基本数据类型转换成包装类;通过intValue方法、doubleValue方法等来自动拆箱,将包装类自动拆箱为基本类型。
- Integer缓存机制了解吗?
Integer缓存机制:Integer先对常使用到的数据进行了缓存,预先为-127~128的数字创建了对象。当使用到范围之内的Integer数据时,将直接指向他在缓存中的地址。
因此两个处于缓存范围之内并且有相等Integer值,将指向同一地址,用==比较时,返回的是true。
- 怎么避免浮点数精度丢失的问题?(了解)
用BigDecimal方法,并设置需要的精度(保留的小数),保留数据的方法(四舍五入)
- 如何比较两个BigDecimal是否相等?(了解)
使用compareTo方法进行比较,他们将不进行精度比较
而equals方法将会比较精度
基本数据类型
byte short int long char float double Boolean
字节: 1 2 4 8 2 4 8 1
Boolean默认为false
long,float,double赋值时加上 L F D
(一个字节=8比特位)
整数类型与浮点类型数据计算,结果为浮点型。
自动转换
精度小的转换成精度大的数据类型时,会自动转换
强制转换
直接( )强转
*向下转型也是强转转换
引用的概念:
装箱和拆箱
装箱:将值类型转换成引用类型,如:int型转换成integer
拆箱:将引用类型转换成值类型,integer-> int
jdk5后自动装箱拆箱(有例子)
自动装箱通过Integer.valueOf()完成(或者说valueOf方法会使用到缓存机制),自动拆箱通过Integer.intValue()完成
但是装箱会创建对象,频繁装箱会消耗内存,应该尽量减少装箱
细节题:
true--包装类和基本类型比较时,包装类会自动拆箱为int型再进行比较
true--两个Integer包装类进行比较,先看数值是否在-128~127的缓存中有相应对象,如果在范围内,则不用创建对象。(Integer,Short,Byte都有缓存机制,并且范围都是-128~127)
false--c,d在先前定义为Integer类型,并且200不在缓冲范围内,所有创建了两个对象
比较基本数据类型用==,比较包装类用equals
注意:
引用类型和引用数据类型没有区别(程序员偷懒,习惯叫引用类型)
对象的引用可以理解为指向对象的指针
long和Long是有区别的!!!
包装类都是引用类型!
TreeSet 不使用 hashCode 方法,也不使用 equals 比较元素,而是使用 compareTo 方法
装箱拆箱应用场景:
- 用==比较包装类和算数表达式时,包装类会自动拆箱
- 调用一个包含object参数的方法时,传入int会自动装箱
BigDecimal:
使用BigDecimal计算或表示浮点类型时,务必使用字符串的构造方法来初始化BigDecimal。
上图的个人理解:
用BigDecimal(double)构造方法传入的double值会与实际不符,直接导致精度丢失
正确做法是:
1.将double数值写成字符类型,并打上""。
2.使用BigDecimal的valueOf方法,valueOf方法里又执行了toString方法,但toString截断的数据,已经是double类型的极限。
BigDecimal的加减乘除:
add subtract multiply divide
加 减 乘 除
a.compareTo(b)方法:
返回值: a<b: -1 a=b: 0 a>b: 1
等值比较
equals会比较精度 compareTo会忽略精度
包装类
为什么要有包装类?
答:java是一个万物皆对象的语言。而基本数据类型是例外,无法调用类和方法
基本数据类型和包装类的区别:
- 包装类有null值,基本数据类型没有。(数据库的查询结果可能为null)
- 包装类可以用来指定泛型,但是基本类型不可以。List<Integer>list=new ArrayList<>( );
- 基本数据类型更高效。基本类型直接在栈中存放具体值,占用的空间没有包装类多,使用起来也不用去new
- 两个包装类的具体值可以相等,但是对象名不同,用==判断两个不在缓存中的包装类时,他们的地址值不同。
常见类
- Object类的常见方法有哪些?
toString。equals。wait。hashcode。finalize。getClass
- ==和equals()的区别是什么?
==在比较基本数据类型时,单纯比较是否等值。比较引用数据类型时,比较的是地址值。
equals只能用来比较引用数据类型。如果equals没有被重写,则于==相同;如果重写了,按照重写后的标准来比较。
- 为什么要有hashCode?
hashcode是为了存储引用数据类型时,返回一个int整数,来确定该存储的位置。hashcode可以快速找出数值在所处集合中的位置。hashcode还能用来确定是否为相同对象:多次比较对象的哈希值相同,则一点是相同对象。
- hashCode和equals区别是什么?
hashcode返回一个哈希值,哈希值相同的两个对象为相同对象,出现特殊情况则叫做哈希冲突。
equals用来比较两对象的引用地址值是否相同,如果没有重写equals方法,则于==无异。
- 为什么重写 equals() 时必须重写 hashCode()方法?
老师,这题我会。equals方法判断引用数据类型时,先执行的是hashcode方法,如果hashcode方法返回的是false,那就没有进行下去的必要了,equals也直接返回false。而且,在Map集合中,如果比较具有相同属性值的两个对象时,没有重写hashcode方法,将会执行object中的hashcode方法,发现两对象的引用不同,将视为不同对象,而打印Map集合时,发现出现了重复的值,而这在Map集合是不允许的(Map无索引、不重复)
- 浅拷贝和深拷贝有什么区别?
浅拷贝拷贝的是对象的索引,当原来对象的值改变时,浅拷贝的对象值也将改变。
深拷贝是直接新建一个引用类型并将对应的值复制给它。这样,修改值也不会影响到深拷贝。
- 深拷贝有几种实现方式?
不知道哎!!!
- String、StringBuffer、StringBuilder的区别?
String是final修饰的不可变类,由final char[]组成,拼接和切割操作都将产生新字符。
StringBuffer是线程安全的可修改字符序列,没有线程安全的StringBuilder在字符拼接上更高效、更常用
object类
方法:
clone()返回对象的一个拷贝、equals()判断对象是否相等、finalize()垃圾回收、getClass()获取运行时的对象的类、hashcode()获取对象的哈希值、notify()唤醒在等待的线程、notifyAll()唤醒所有线程、toString()获取对象的字符串表示形式、wait()让当前线程加入等待状态
重写equals时要重写hashcode方法:
- 如果没有重写hashcode方法:
以set集合里两个new出来的java,18 、java,18为例
set先要进行去重,会先用hashcode判断两个对象的哈希码是否相同,但是你没有重写,
所以我用的是Object里的hashcode方法,hashcode比较两个对象的引用地址是否相同,
显然这里是不同的(因为两个都是new出来的),hashcode方法判断你都没通过,返回了false
那我就没必要对你进行equals判断了。这就证明你们两个不是相同对象,所以set不会对你们去重。
控制台将输出两个java18,java18.。。所以:在set集合里,要是你只重写equals,不重写hashcode,
将会导致存储两个相同属性的值。(set无序、不重复)
- 如果重写了hashcode方法:
还是先hashcode对两个对象进行判断,重写的hashcode方法里,假设写的是判断name和age是否相等,则hashcode验证通过,返回true。
再进行equals判断,两个对象通过了equals判断,又返回true,这时set知道了,你们是相同对象,那set就不存储相同对象了,进行去重,所以只打印一个java18。
- 注意:
利用hashcode和equals协同判断两个对象是否相等,可以提高程序的插入和查询速度。如果重写equals方法,不重写hashcode方法,
程序会先用hashcode进行判断,而在set集合中,一下就得出两对象为不同对象的结论,直接给set干蒙蔽了,输出了两个属性值相同的对象。
通常我们会放宽政策,认为只要属性值都相同,那就是同个对象。
- hashcode和equals的规范:我们应该确保
1.hashcode和equals返回值是稳定的,不能有随机性
2.两对象在==判断为true时,equals判断也应当为true
3.两对象equals,则它们的hashcode也应该相等
深拷贝和浅拷贝:
引用拷贝:只会复制对象的地址,并不会创建对象。
浅拷贝:按位拷贝对象,会创建一个新对象,里面有原始对象属性值的精确拷贝。但是浅拷贝在拷贝引用数据类型时,修改一个对象的引用类型的值,还是会影响到拷贝。因为浅拷贝只会复制引用类型对象的地址,原对象和浅拷贝指向同一个地址。
深拷贝:深拷贝会拷贝全部属性(包括引用类型的属性),并拷贝属性指向的动态分配的内存。对原对象的修改将不会影响到深拷贝。深拷贝会创建拷贝类的一个新对象,从而与原始对象相互独立。
序列化对象采用深拷贝
集合一般用浅拷贝
String类
String介绍:String被final关键字修饰,表示不可继承。String存储在被final修饰的char[ ]中,表示String对象不可修改。
如此设计String的好处:
- 保证String对象的安全性
- 保证哈希值不会频繁变更
- 可以实现字符串常量池:
如果写String str="abc"; 虚拟机会先检查字符串常量池是否存在,如果有,直接返回对象引用,节约空间。
字符电话题目:
String可以用charAt( )获取索引字符,
单个字符:
如果要转换成数字:charAt( ) - '0' 就好像是把字符的衣服脱了,变成了数字。
多个字符:Integer.parsentInt( )
字符串小细节:
java会用StringBuilder的方式优化字符串变量的拼接。
字符串拼接用append方法优于+加号
StringBuffer由于保证线程安全,需要竞争锁,从性能上,比不过StringBuilder。
indexOf()和split()都能实现字符串切割
String、StringBuffer、StringBuilder 区别
String被声明为final,拼接、裁剪操作都会产生新字符串对象。
StringBuffer是线程安全的可修改字符序列,线程安全主要是通过在修改数据的方法上,用synchronized关键字实现
StringBuilder在java1.5时新增,相比StringBuffer少了线程安全,也因此减小了开销,是字符串拼接首先。
StringBuffer和StringBuilder初始长度都是16,也可指定初始长度。
异常
- 介绍Java的异常体系?
Throwable为超类,分为Exception异常和Error错误。Exception分为可检查异常和不检查异常。可检查异常需要显示的捕获异常。
- Exception和Error有什么区别?
Exception和Error都是Throwable的子类,Error是错误异常,属于程序中比较严重的问题,如栈溢出、虚拟机运行异常。而Exception分为检查异常和非检查异常,其中非检查异常通常是程序的逻辑错误,检查异常必须显示捕获。
- 如何自定义异常?
定义异常类继承异常,类中定义一个无参构造,一个有参构造
异常定义:
java异常是java提供的识别及响应错误的一种机制,可以分离正常代码和异常代码,提高程序健壮性。
异常层次机构
处理异常的两种方法
- try - catch 直接解决异常
- throws 向上抛异常
Error:
程序中无法处理的错误,表示程序运行时出现了严重问题。如:StackOverflowError栈溢出错误、OutOfMemoryError内存不足错误、Virtual MachainError虚拟机运行错误、NoClassDefFoundError类定义错误;
Exception异常:
Exception分为运行时异常和受检异常(编译时异常):
运行时异常: (编译时不强制要求处理,通常是程序逻辑错误或无法预见的异常 )。
受检异常(checked)(编译时异常):(设计目的是确保程序员在编写代码时考虑并处理可能出现的异常情况,从而提高程序的健壮性。 *必须显示地进行捕获”即有明显的try-catch“,为了保证在运行时不会因为这些未处理的异常而导致崩溃。)。
记忆:非检查,逻辑错;是检查,要捕获。
方法签名(Method Signature)是指方法名称和参数表组成的方法标识符,是标识一个方法的基本方式
int sum(int a, int b)
这个方法签名为:
方法名:sum
参数表:(int, int)
返回类型:int
所以整个签名可表示为:
sum(int, int): int
Exception和Erroe的区别
Exception是程序正常运行中,可以预料的异常,应当被捕获并进行异常处理。
Error是程序非正常运行的情况,如程序死亡,jvm运行错误等。既然非正常运行,Error也就也就难以捕获
throw和throws区别
throw用于抛出异常,写在函数内。执行到throw时,功能就已经结束了并跳转到调用者,将具体问题返回给调用者。所以throw语句独立存在时,下面的语句就不会执行了。
throws用于声明可能抛出的异常。
异常的关键字:
try:用于捕获,将可能会发生异常的代码当在try中,如果发生异常,就抛出(catch就会去捡)
catch:用于捕获try中发生的异常。
finally:强制执行的语句。常用于回收资源。
throw:用于抛出异常。
throws:声明可能抛出的异常。
如何自定义异常类: 见csdn收藏《如何自定义异常》
自定义异常类要继承于某个异常,并且类中包含无参和有参构造,有参构造打印异常详情描述。
try必不可少,可以和finally、catch单独组合。
catch捕获多个异常时用 | 隔开。
异常的执行顺序:
- 没有捕获到异常,将执行完try后,直接跳转到finally
- try中捕获到异常,但是catch没有处理此异常的情况,那虚拟机就会接手,执行完finally后,finally后面的代码也不再执行了。(try-finally类型:try引起异常的代码 之后的代码不会再执行,直接finally)
- 当try有异常,就会在catch语句逐一匹配,匹配以后,catch后面的语句不会再执行,而try当中,位于异常以后的代码也不会再执行了,跳转到finally语句。
泛型
Java 泛型:泛型和类型擦除详解-修改中..._泛型擦除-优快云博客
- 什么是泛型?有什么作用?
泛型就是能广泛适用的类型,泛型就是将数据类型参数化,把数据类型转化为一个指定参数。
泛型能让运行时可能会出现的数据类型安全问题提升到编译期间,并且,泛型避免了自动装箱拆箱的行为,提高了性能。如果不使用泛型,则需要进行需要强制转换。
- 泛型原理是什么?
将数据类型参数化,通过类型擦除的方式实现。即在编译期间进行泛型擦除,并作出相应的类型转换。
什么是泛型
泛型就是能广泛适用的类型。泛型的本质是将数据类型参数化,所操作的数据类型被指定为一个参数,这个参数可以用在方法、类、接口中。称为泛型方法、泛型类、泛型接口。泛型还提供编译时类型安全检测。
泛型的作用
- 把只有在运行时才能发现的错误提前到编译时就能发现,及时解决。
- 消除强制类型转换,提高程序性能:(源码中,数据在return之前会根据泛型变量进行强转)
<泛型类型>的命名习惯
集合E、 类T、 kv键值对、N数据类型、 ?不确定型
比如:Number是 Integer和Float的最小相同父类
泛型类、泛型方法、泛型接口
泛型类:
- 类名后面要指定泛型
- get方法的返回值类型就是泛型类型
泛型方法:
(并不仅限于存在泛型类中)
格式:public <T> T func(T t){ } public、参数类型、返回值、方法名、形参
方法写在类中,main方法直接调用
泛型接口:
非抽象类必须实现接口里的所有方法
mian方法里面实现接口:=new 类名<>( ); 等号右边不是jiekou
泛型的实现原理???
泛型本质上是数据类型 参数化,通过擦除的方式来实现,即会在编译期间擦除泛型语法,并做出相应的类型转换。
类型擦除是什么
在使用泛型时,加上的泛型参数会在编译期间被去除。java泛型的实现是在编译层,编译后的字节码文件中将不包含泛型中的类型信息。
注意:
1.泛型中的参数化类型没有继承关系
public static void main(String[] args) {
ArrayList<Object> objecList = new ArrayList<Object>(); // 编译通过
objecList.add(new Object());
// Object类型转String类型,会抛出类型转换错误
ArrayList<String> list1 = objecList; // 编译错误
}
2.泛型类型变量不能是基本数据类型
比如ArrayList<double>(错误),类型擦除后,ArrayList的原始类型就变为Object,但是Object不能存储基本数据类型double,能存储引用类型Double。
3.不能创建泛型类型数组
如List<String>[] stringLists=new List<String>[]; // 编译报错
类型擦除后,参数类型变为Object,应当可以添加任何类型,不应在后面再指定String
4.不能实例化泛型
因为new无法为不确定类型分配内存空间
题外话:
什么是引用?
比如 A a = new A();
此时变量a指向了一个A对象,a被称为引用变量,也可以说a是A对象的一个引用。我们通过操纵引用变量a来操作A对象。变量a的值为它所引用对象的地址。
API
API允许开发者在不了解内部实现的情况下使用某个软件组件的功能。
反射
学会基本api,知道利用反射可以做什么事情
什么是反射
反射是java机制,能够动态获取信息和调用对象方法
反射原理
当一个字节码文件加载到内存中,虚拟机会对字节码文件进行解剖,创建一个对象的class对象,虚拟机将该对象的全部信息都存储到class对象中,我们只要获取到class对象,就能使用该对象来操作属性、调用方法等。
反射中的方法
(1)判断是否为某个类的实例
1.instanceof关键字 2.isInstance方法
(2)创建实例
1.class对象的newInstence方法
2.构造器对象的newInstence方法
反射面试题
1.除了new可以创建对象,还有什么可以?
利用反射可以创建对象。
2.new和反射,谁创建对象效率高?
new在编译期即可确定要加载的对象路径是否合法;反射创建对象时,先查找资源类,再使用类加载器创建对象,在运行期间对 安全合法性 的检查影响了类加载的效率。
3.反射的作用
反射机制在运行时,对于任何一个类,都能知道这个类的属性和方法;对于任何一个对象,都能调用它的所有方法。只需要给定类名,就能通过反射来获取类的所有信息。这种动态获取信息和动态调用对象方法的功能叫做java语言的反射机制。
4.哪里会用到反射?
框架开发、动态代理、对象序列化
5.反射的实现方式有哪些?(获取class对象的方式有哪些?)
1)Class.forName("类的路径") 静态调用本地方法 JDBC中常用此方法加载数据库驱动
2)类名.class
3)对象名.getClass( )
4)调用 ''基本类型的包装类'' 的Type属性,来获取基本类型包装类的class对象。
5)通过类加载器,再传类的路径进去xxxClassLoader.loadClass()
ClassLoader.getSystemClassLoader().loadClass("类的路径");
6.反射的优缺点
优点:
能够在运行时,动态地获取类的信息和动态调用对象的方法,提高灵活性。
缺点:
破坏封装性,反射调用方法时可以忽略权限检查,将会导致安全问题。
使用反射创建类的效率低于new,需要解析字节码文件,对内存中的对象进行解剖。
解决方案:
1.通过setAccessible(true)关闭JDK的安全检验来提升反射速度。
2.多次创建一个类时,可利用缓存加快反射速度。
7.*反射常用的API
反射API用来生成JVM中的类、对象、接口的信息
1.Class类:反射的核心,获取类的对象和方法。
2.Field类:Java.lang.reflec包中的类,表示类的成员变量,用来获取和设置类中的属性的值。
getFields:获取所有 public修饰的属性,包含本类以及父类的
getDeclaredFields:获取本类中所有属性(成员变量)包括protecte、private、默认修饰的成员变量,但是不包括public修饰的成员变量。
3.Method类:Java.lang.reflec包中的类,表示类的方法,用来获取方法信息或者执行方法。
getMethods:获取所有 public修饰的方法,包含本类以及父类的
getDeclaredMethods:获取本类中所有方法,包括protecte、private、默认修饰的
4.Constructor类:Java.lang.reflec包中的类,表示类的构造器。
getConstructors:获取所有 public修饰的构造器,只包含本类的
getDeclaredConstructors:获取本类中所有构造器,包括protecte、private、默认修饰的
动态代理
定义:
一种方便运行时动态构建代理,动态处理代理方法调用的机制。
出现的原因:
静态代码使得类的规模增大,并且不易维护,其中的proxy只是一个中介作用,导致系统结构臃肿。
于是动态代理在运行过程中就可以动态的创建proxy,用完就销毁,避免了冗余问题。
和坤坤一起学动态代理
中介通过接口来找到有唱跳方法的助理
鸡哥和代理都要实现包含唱跳方法的接口
star是接口,包含所有想要被代理的方法(接口里面都是抽象方法)
而且这个例子的代理对象就是一个star接口
代理类ProxyUtil里面有创建代理的方法createProxy,得到一个Star类型的接口;
创建Star是通过newProxyInstance方法实现的,传递了三个参数,第三个参数为重写的invoke方法,里面还有三个简单的参数,分别指定了代理对象是谁,要代理的方法是什么,代理的方法要传递的参数是什么
每一个动态代理类都必须实现Proxy类和InvocationHandler接口
Proxy类
Proxy类的作用:动态创建代理对象。常用newProxyInstance方法(生成一个Object动态代理对象)实现。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,
InvocationHandler h) throws IllegalArgumentException
loader:指定用哪个类的加载器,并且再让他去加载代理(通常是先获取类名, 类名.getClassLoader)
interfaces:代理要用到的接口数组,表示生成的代理长什么样,有哪些方法。
h: InvocationHandler接口的对象,表示我的动态代理对象在调用方法的时候,会联系到哪个 InvocationHandler接口对象。
InvocationHandler接口
用来指定代理需要做什么,一般在newProxyInstance里面重写为匿名内部类
//invoke写在动态代理类中
当我们通过代理对象调用一个方法时,方法的调用会被转发为由InvocationHandler接口中的invoke方法来调用。 代理接到活儿了,直接传给接口中的invoke方法,看看你要代理什么业务。搭建舞台、收钱、调用真实对象的方法(让明星唱歌)。
invoke的三个参数:
// 代理要做什么事情,是由invoke决定的
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
proxy:代理类的对象(小助理)
method:想要调用小助理的哪个方法?(虽然小助理也有这些唱跳方法,但是实际都是叫坤坤去)
args:调用小助理的method方法需要传递的参数 (让坤坤唱只因你太美)
动态代理的优缺点
优:比静态代码块简约,代码复用率高
缺:强制要求实现InvocationHandler接口
注解
- 什么是注解?作用是什么?
注解是一种元数据,属于java数据类型。注解是一种标记,在编译器或运行时检测到这些标记就会触发相应的操作。 注解用在类、方法、接口、成员变量等当中,可以减少代码冗余,可以抑制编译器的警告,可以指定在哪些位置使用标记。
- 怎么自定义注解?
自定义注解中只能写public或者省略,后面接@interface再加注解名。注解当中写注解类型元素,注意与接口中的参数不同,注解类型元素可以定义默认值default,如果不定义,则后续使用需要赋值,通常在只有一个注解类型元素时将其定义名为value;特别:注解的方法当中不能传参,只能写空( );注解之间可以嵌套。
注解的定义
注解是一种元数据形式,即注解属于java的一种数据类型,类似接口、类、数组。
注解也是一种标记,在编译或运行时期检测到这些标记就可以执行一些特殊操作。
注解可以用来修饰类、方法、变量、参数、包。
注解不会对修饰的代码直接产生影响。
解析注释的方法:
1.编译器直接扫描:
java编译字节码文件时,如果检测到某个类或者方法被注释修饰时,会对其进行处理
2.运行时的反射:
编译器无法处理自定义注解,需要利用反射来识别注解携带的信息,做出相应处理。
注解的作用
1.编译器通过注解来检测错误信息或者抑制警告。
2.程序可以处理注解信息以生成代码、XML文件等。
3.运行时也能检查或处理注解。
4.注解可以减少配置文件和代码。
注解的不足
1.注解是一种侵入式编程,会增加代码的耦合。
2.自定义注解使用到反射,如果注解修饰的是非public成员,也能通过反射获取,但是破坏了面向对象的封装性。
注解的形式:
1.包含多个属性
2.只有一个名为value的属性,属性名字可以省略
3.标记注解:该注解没有属性 如:@Override
内置注解
- @Override
表明方法重写了父类方法。如果重写的不是父类方法,或者不小心写错了,java编译器会警告。
- @Deprecated
表明该方法已经废弃、过时,不建议使用。父类的@Deprecated可能会继承给子类。
- @SuppressWarnnings
用来关闭警告。@SuppressWarnnings是String[ ]类型数组,里面存放要关闭警告的类型。
常见的有:deprecation、unchecked、path、finally、all等
例如:deprecation
- 使用了不赞成使用的类或方法时的警告;
@SafeVarargs
(JDK7 引入)
抑制与可变参数Varargs泛型方法相关的堆污染警告。
(堆污染:运行时,存在类型不一致所导致的安全问题)
@FunctionalInterface
(JDK8 引入)
如果使用了该注解,编译器就会检查该接口中的抽象方法是不是只有一个,如果有多个就会报错
自定义注解
1.先创建具有@interface关键字的自定义接口模板
2.再定义注解类型元素(不同于抽象方法的地方是,这里有default修饰的默认值)
注意:定义注解类型元素时
- 访问修饰符必须为public
- 注解元素类型只能是基本数据类型、String、Class、注解(嵌套)、数组
- 约定俗成:如果注解类型元素为注解,且只有一个元素,用value起名
- 定义方法里不能传参,只能写一个( )
- 没有默认值的话,后续使用注解必须赋值
自定义一个名为MyAnnotation的自定义注解
用在Person类中,不想使用默认值hello,改成了class
元注解meta-annotation
JAVA核心知识点--元注解详解_java 元注解 从哪里来的-优快云博客
定义:对现有的注解进行解释说明的 一种注解。
下图中,@Target和@Retention 都是zidingyi的元注解
有哪些元注解?
- @Target:描述注解修饰的是谁,类?成员变量?
格式:@Target({TYPE,FIELD}) 参数大写,用{}包含,不需要分号
TYPE, // 类、接口、枚举类
FIELD, // 成员变量(包括:枚举常量)
METHOD, // 成员方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类
PACKAGE, // 可用于修饰:包
- @Retention:描述注解保留的时间范围
格式:@Retention(PetentionPolicy.SOURCE)
SOURCE //只在源文件保留,所有编译器不会记录实现了该元注解的注解信息。
CLASS //编译期保留(默认值)
RUNTIME //运行期保留,可通过反射获取注解信息
- @Documented:描述在使用javadoc工具为类生成帮助文档时,不保留其注解信息
- @Inherited:被Inherited修饰的注解具有继承性。
SPI
- SPI 是什么?
SPI是java提供的让第三方进行实现、拓展的服务接口。
- SPI 有什么好处?
SPI在JDBC中,在处理不同JDBC驱动时,不需要引用具体的实现类,只需要将驱动Jar包放在类路径当中,SPI会自动实现合适的驱动。
定义:
Service Provider Interface,提供服务的接口。就是java提供的一套用来被第三方实现或拓展的接口,实现了动态扩展接口,第三方的实现能像插件一样嵌入到系统中。
SPI和API的区别:
SPI更像是老大,要求自己的小弟(服务提供者)都按这个标准去实现自己的接口,等老大不喜欢这个接口了,就可以随意调用其他小弟的接口(框架能够灵活地在多个实现之间进行切换)。
API更像是两个人约定好了如何去实现接口(对接口进行了标准化),规定好了不同组件之间要如何进行功能调用。
应用:
JDBC:各个数据库厂商提供自己的JDBC驱动实现,应用程序无需直接引用具体驱动的实现类,只需将驱动JAR包放入类路径,Java SPI机制会自动发现并加载合适的驱动。
Spring:特别是在某些扩展点和与第三方库集成时中有所使用。
全限定名:包名.类名,中间用 . 隔开
非限定类名是:T 全限定类名是:mybatis.T
SPI规定 :
服务实现者需要在 META-INF/services/ 目录下 新建文件名为 SPI接口全限定类名的文件
文件内容为 服务实现者需要被加载的具体类的全限定类名。
如:
好处:
1.实现解耦,将接口与具体实现分离开
2.提高框架的扩展性
File类
常见API:
File():拼接路径/把路径转为文件对象
length():返回文件大小
getAbsolutePath():返回绝对路径
getPath():返回定义文件时的路径
getName():返回文件名,包括后缀
lastModified():返回最后一次修改文件的时间
createNewFile():创建文件夹。如果已经存在,将返回false
mkdir():用的不多:在某父级文件下创建一级文件。
mkdirs():创建一个多级文件
delete():删除 文件(不存放在回收站,直接永久删除) / 文件夹(如果文件夹为空才可删除)
I/O
- 什么是序列化?什么是反序列化?
序列化就是将对象数据转化成二进制
反序列化就是将二进制数据转化成对象
- Java是怎么实现序列化的?
实现序列化首先要实现serializable标记接口,再选择默认序列化或是自定义序列化。
- 常见的序列化协议有哪些?
XML、JSON、Thrift
其中XML是一种标记语言,具有很强的拓展性,可以自定义标签,用来满足各种数据的表示需求。
Json通常用于web应用,使用文本格式来结构化数据,便于人们理解、编译,也便于计算机解析、生成。
Thrift是一种跨语言的服务框架,具有远程服务调用RPC的解决方案,实现不同语言服务之间的通信。
- 重点掌握网络I/O:
-
- BIO/NIO/AIO 有什么区别?
BIO同步阻塞IO,是传统IO流,以流为基本单位,包括字节流和字符流。阻塞:例如未读取完成,就不可以进行其他操作,只能等待读取结束。
NIO同步非阻塞IO,以块为基本单位,传输数据效率高,非阻塞:线程无需等待操作结束,直接返回,进行下一步操作。
AIO异步非阻塞IO,异步:调用者A调用B,B直接返回,但是没有结果,当有结果时再通知调用者A,A是被动接受处理结果的。
-
- 什么是NIO?
NIO同步非阻塞IO,以块为基本传输单位,包含三个重要组件:Buffer、Channel、Selector。其中一个通道对应一个缓冲区。选择器使得NIO的一个线程可以处理多个操作。
-
- I/O多路复用是什么?
多路复用就是一个线程同时监听多个事件,当有读写操作完成时,就通知应用程序去处理该事件。
-
- select 和 epoll 有什么区别?
select将文件描述符存储在集合中,但最多只能存1024个,每次需要遍历fdset找出可读写的非负返回值。
epoll是将文件描述符存储在红黑树中,而且可存储的文件描述符无限,查找时间复杂度为o(1),并将就绪事件存储在就绪链表当中,当有操作完成时,就调用就绪链表当中的事件。适合大规模开发、高性能场景。
序列化与反序列化:
序列化的出现:
在网络中传输的数据必须是二进制的,但是调用方都是以对象的方式请求出入参数,对象不能在网络中传输,我们需要将对象转化成可在网络中传输的二进制,并且保证此过程可逆。
序列化:
将对象转换成二进制;
ObjectOutputStream:对象输出流(高级流)。嵌套一个基本流FileOutputStream。
writeObject方法:把数据序列化后写到文件中。
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("路径"));
oos.writeObject(想要序列化的对象);
反序列化:
将二进制转换成对象;
ObjectInputStream对象输入流(高级流),需要嵌套基础流FileInputStream。
再用readObject( )读取。
ObjectInputStream ois=new ObjectInputStream(new FileInputStream("路径"));
//读取数据时,用Object o接收读取到的数据。
Object o=ois.readObject(); //也可以强转成对应类
System.out.println(o);
ois.close();
注意:
1.要实现序列化的话,传输数据所属的类,要先实现Serializable接口(表示当前类可以被序列化)否则将出现NotSerializableException异常。
2.序列化流写到文件中的数据不可修改,否则无法读取。
3.序列化以后,如果修改javaBean,会出现InvalidClassException异常。解决方案是:在javaBean中添加序列号SerialVersionUID。
4.如果某个成员变量不想被序列化,可用transient关键字修饰。static关键字修饰的也不会被序列化。
常见的序列化协议:XML、JSON...
XML:
(eXtensible Markup Language)是一种标记语言,用于存储和传输数据,具有很强的扩展性,可以通过自定义标签来满足各种数据的表示需求。与HTML类似,XML使用标签来描述数据,但XML更注重数据的结构和内容。
JSON:
(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人们阅读和编写,也利于计算机解析和生成。JSON常用于web应用。它使用文本格式来结构化数据。
Thrift:
一种跨语言的服务开发框架,包含序列化在内的一套远程服务调用(RPC)解决方案。方便在不同语言编写的服务之间进行通信。
还有Avro、Protobuf(高效的二进制序列化协议,序列速度快)
I/O:
记忆:从谁读取input,向谁写入output
同步/异步:(孤身一人/与人协作)
同步:是一种有序运行机制,需要等一个调用返回,才能进行下一步。
异步:任务之间不需要等待调用的返回,通过一种机制来决定任务之间的次序关系。
阻塞//非阻塞:(小时候烧水:眼巴巴看着水烧开 // 长大后烧水:烧水时还可以去刷抖音)
阻塞:当进行会发生阻塞的操作时,当前线程会进入阻塞状态,无法进行其他任务。
如: 写完了数据才能进行读取。
非阻塞:发起IO操作时,线程无需等操作完成,直接返回,继续处理其他操作。
线程(Thread):
轻量级进程,是操作系统进行调度的最小单位。一个线程是一个任务(一个程序段)的一次执行过程。线程不占有内存空间,它包括在进程的内存空间中。在同一个进程内,多个线程共享进程的资源。一个进程至少有一个线程。
BIO:
blocking IO,同步阻塞式IO,指传统的java.io包,基于流模型实现。
先关闭外层流,外层流的关闭会自动对内存流进行关闭。所以内存流的关闭可以省略。
NIO:
Non-Blocking IO,同步非阻塞 IO,在java.nio包
与BIO不同的是,NIO是基于块的(内存上是一组连续的空间),以块为基本单位处理数据,其中最重要的是三个组件:Selector选择器通道Channel和缓冲区Buffer、。一个Channel对应一个Buffer。Selector使得NIO的一个线程可以处理多个操作。
AIO (NIO2):
对NIO的改进,异步非阻塞(Asynchronous IO)
BIO与NIO的区别:
BIO以流的方式处理数据,NIO以块的方式处理数据。块的效率比流的效率高。
BIO是阻塞的,NIO非阻塞。
BIO是基于字节流和字符流进行操作。NIO基于通道Cannal和Buffer缓冲区进行操作,从通道读取数据,存到缓冲区;或者从缓冲区写到通道。selector选择器同来监听多个事件,使得单个线程就能操作多个客户端通道。
IO多路复用:
定义:
一个线程同时监听多个事件的方法。当任何一个文件描述符上有数据可读或可写时,IO多路复用会通知应用程序去处理该事件。常见的有select、poll、epoll。
(文件描述符 fd:用于标识和访问打开的文件或设备,是一个非负整数,可用于寻址)
select:
select通过文件描述符来管理多个IO通道。应用程序在调用select时,需传入一个文件描述符集合(fdset例如读集合、写集合、异常集合),当有IO事件发生,select返回相应的文件描述符集合(此过程需要遍历文件描述符集合fdset),再进行下一步处理。但是select受文件描述符的数量限制,最大为1024,所有select最多监听1024个文件描述符。
poll:
poll使用链表来存储文件描述符,解决了监听数量上的限制,但是依旧需要遍历fdset。
epoll:
epoll采用事件通知机制,只返回发生变化的文件描述符,而不是遍历文件描述符集合。
epoll的三种核心操作:
epoll_create:创建一个epoll实例,用来管理多个文件描述符。
epoll_ctl:添加、修改、删除epoll实例中的文件描述符事件。
epoll_wait: 阻塞等待事件触发,并返回已经就绪的文件描述符。
epoll的好处:
时间复杂度为o(1):不需要遍历文件描述符集合,只返回变化的文件描述符。
红黑树和就绪链表:将监控的文件描述符存储在红黑树中。某个文件字描述符发生变化时,就会被存放到就绪链表,高效管理和返回事件。
poll和epoll对比:
poll采用链表来存储文件描述符,解决了数量上的限制,但是还是需要遍历,性能随着必发量增加而增加。
epoll采用有序链表和红黑树,只返回读写发生变化的文件描述符,时间复杂度为O(1)。适合高并发。
三者对比:
设计模式(后续再看)
- 谈谈你知道的设计模式?
- 手撕单例模式? (双检锁单例模式要会默写)
- Spring等框架中使用了哪些模式?
Java8 新特性
- lambda有什么好处?应用场景是什么?
lambda可以简化代码,代替匿名内部类,用来遍历,在stream流中进行操作等。
使用场景有:遍历、创建多线程、在GUI图形化界面中更好处理事件、用于Stream流
- 方法引用是什么?有什么好处?应用场景是什么?
方法引用就是对已有方法的再次实现,达到简化代码,节约内存的好处。在函数式接口中、GUI编程、lambda表达式中都有方法引用的应用场景。
- Java中的 stream流知道吗,有哪些功能?
stream是对代码的一种简化,达到对数据筛选的目的。其中包含许多方法:创建方法、中间方法、终结方法。可对流数据进行筛选获取、判断元素存在与否、满足条件的数据个数等功能。
- stream 应用场景是什么?
数据处理、映射转换、排序、并行处理。。。
lambda表达式
lambda表达式也成为闭包,是推动java8发布的重要特征。
是一种可以接收多个参数并且返回单个表达式值的匿名函数。(lambda表达式不能包括命令)
为什么使用lambda表达式
lambda表达式可以理解为一段可以传递的代码,允许把一个函数作为方法的参数,具有简洁灵活的特点。
lambda语法
操作符”->“,操作符的左侧指定lambda表达式需要的所有参数,右侧表明要执行的功能。
lambda表达式的好处
1.替代匿名内部类
2.对集合进行遍历和筛选
3.与streamAPI结合使用,从而并行处理,提高编程性能。
4.支持函数式编程风格,使代码更加灵活可维护。
应用场景:
- 图形用户界面GUI编程中,lambda用于事件处理。如:按钮点击事件
- 在操作集合时,lambda结合streamAPI进行过滤排序等。
- 回调函数(函数它作为参数传递给另一个函数)时,简化代码
- 多线程编程中,用于创建线程和任务。
方法引用 ::
引用方法就是将已经有的方法拿过来使用。可以看作是lambda表达式的简化,当lambda表达式只是调用了一个已经存在的方法时,就可以使用方法引用。
方法引用的使用格式:
一般 : : 后面放成员方法,或者是new
举例:
引用静态方法: 类名: :方法名 (方法名后面不用打括号)
使用前提:
方法体只有一行代码,这个代码调用了某个类的静态方法,并且我们重写的这个抽象方法的所有参数都按照顺序传入了这个静态方法中。
+++++++++++++++++++++
// 定义一个函数式接口,里面有一个抽象方法进行数值计算
@FunctionalInterface
interface Calculator {
//接口里的方法都是抽象方法
int calculate(int num1, int num2);
}
// 定义一个工具类,里面有静态方法用于具体的计算逻辑
class MathUtil {
public static int add(int a, int b) {
return a + b;
}
}
// 实现类,通过静态方法引用实现接口中的抽象方法
class CalculatorImpl implements Calculator {
@Override
public int calculate(int num1, int num2) {
//原本:return MathUtil.add(int a,int b);
// 因为方法体只有这一行代码,且调用了MathUtil类的静态方法add,并传入了calculate方法的所有参数
return MathUtil::add;//<--使用方法引用
}
}
我对使用前提的理解:
1.只有一行代码:
这就是为什么写成return MathUtil.add(int a,int b);
而不是int result = MathUtil.add(num1, num2);
return result; 的原因。
2.调用了某个类的静态方法,那必须使用到了一个类的静态方法。
3.重写的这个抽象方法的所有参数都按照顺序传入了这个静态方法中:
(首先明确,即使没有抽象方法,也可以进行方法引用,方法引用的存在不依赖于抽象方法的存在。
比如:System.out::println)
这句话是对使用方法引用前说的,使用方法引用前,需要将抽象方法add的所有参数都按照顺序传入静态方法。
但是使用方法引用后是不需要传参的,也不需要( )。直接return MathUtil::add;
引用对象的成员方法: 对象名::方法名
使用前提:
方法体只有一行代码,这个代码调用了某个对象的成员方法,并且我们重写的这个抽象方法的所有参数都按照顺序传入了这个成员方法中。
+++++++++++++++++++++
原来:
List<Author> authors = getAuthors();
StringBuilder sb = new StringBuilder();
authors.stream()
.forEach(name -> sb.append(name));
改成:
.forEach(sb::append)
引用类的实例方法 类名::方法名
使用前提:
如果我们在重写方法的时候,方法体只有一行代码,并且这行代码是调用了第一个参数的成员方法,并且我们把要重写的抽象方法中剩余的所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用类的实例方法。(不理解)
方法引用的好处:
- 代码简洁
- 提高代码可读性
- 增强可维护性
方法引用的应用场景:
- 集合操作中引用stream中的API
- 便于实现函数式接口。
- GUI编程中方便数据处理
Stream流
创建流的方法:
- 单列集合: 集合.stream( )
- 数组: Arrays.stream(数组) 或 Stream.of(数组)
- 双列集合: 双列集合.entrySet().stream()
中间方法:
- filter(): 过滤数据 filter(s->s.getName().length()>2)
- map():对流元素进行转换或计算:map(String::toUpperCase)转为大写
(map多用于获取,getAge,getName...)
.map(author -> author.getAge()) .map(age -> age + 10)获取作家年龄再加10
为什么要写两个map?第一个map干什么的?
答:map里面放不下两个操作,只能第一步先在map里获取年龄。这并不奇怪。
- distict去重
- sorted()排序,先让类实现Comparable接口。sorted((o1,o2)->o1.getAge()-o2.getAge())
- limit()限制最大长度
- skip()跳过几个元素
- concat()延迟串联,流的合并
终结方法:
- forEach()遍历 forEach(student->sout(student))
- count()获取流中的元素个数
- max() min() 获取流中的最值
- collect()将流转换成集合: collect(Collector.toSet()); collect(Collector.toMap(student::getName(),student::getAge()));
- anyMatch()判断是否有符合条件的元素,boolean类型
- allMatch()判断是否所有元素都符合条件,boolean类型
- noneMatch()判断是否都不符合,boolean类型
- findAny()获取任意一个
- findFirst()获取第一个满足的元素
时间复杂度:
用来评估代码的执行耗时。
但是大O表示法不代表具体耗时,而是执行时间随数据规模的变化趋势。
时间复杂度O(1):
只要代码的执行时间不随着n的增大而增大,这样的代码时间复杂度都是O(1):
下图中n再怎么变都不会增加执行时间,都是100次。
大O表示的规则:常量、系数、低阶,可以忽略。
时间复杂度O(n):
public static int sum2(int n){
int sum = 0;
for (int i = 1; i < n; ++i) {
for (int j = 1; j < n; ++j) {
sum = sum + i * j;
}
}
return sum;
}
第一个for循环执行n-1次,第二个for循环执行n-1次,时间复杂度为O(n^2)
时间复杂度O(log n):
再次强调时间复杂度是分析代码执行次数与数据规模n之间的关系。
public void test04(int n){
int i=1;
while(i<=n){
i = i * 2;
}
}
停止条件是2^k>n ,即 k > log₂(n),忽略系数,时间复杂度为O(log n):意味着,当执行次数n增大时,时间复杂度随着n的增大,以''与log n成正比''的速度增长。
时间复杂度O(n * log n):
就是在时间复杂度为O(log n)的情况下,套进时间复杂度为O(n)的循环里。
public void test05(int n){
int i=0;
for(;i<=n;i++){
test04(n);
}
}
public void test04(int n){
int i=1;
while(i<=n){
i = i * 2;
}
}
空间复杂度:
全称:渐进空间复杂度,表示算法占用的额外存储空间与数据规模之间的增长关系。
空间复杂度O(1):
每次只需要对存放sum的数据进行修改,并不需要重新开辟空间。(只需要常量级的内存空间大小,所以空间复杂度为O(1))
public void test(int n){
int i=0;
int sum=0;
for(;i<n;i++){
sum = sum+i;
}
System.out.println(sum);
}
空间复杂度O(n):
传入n,则有n个a[i]的内存申请,空间复杂度为O(n)
void print(int n) {
int i = 0;
int[] a = new int[n];
for (i; i <n; ++i) {
a[i] = i * i;
}
for (i = n-1; i >= 0; --i) {
System.out.println(a[i]);
}
}
集合
集合分为 单列集合Collection 和 双列集合Map。
Collection单列集合:
单列集合Collection分为List和Set(蔡老师CLS)。
List有序可重复,Set无序不重复。
List:
List数组的寻址公式:
arr[i]的地址=数组的首地址+ i * 数组中数据类型的字节大小。
假设下图为int型数组,一个int整数占用四个字节,所以对应四个内存地址,都是整数22的地址值。
如果要找arr[1],就等于1110+1*4=1114;对应数字33。
操作数组的时间复杂度:查询、插入...操作
1.随机查询(索引已知):时间复杂度为O(1)
既然索引已知,我根据首地址和寻址公式就能得到a[i]的地址值。
代码的执行时间不会随着n增大而增大,是常量级别的,时间复杂度为O(1)。
2.未知索引查询:
2.1查找无序数组:时间复杂度为O(n)
因为最快为O(1),最长为O(n),但是一般以平均复杂度和最差复杂度为准,但是无论如何,大O表示法仍然需要去除系数,所以最终,时间复杂度为O(n)。
2.2二分查找来查找排序后的数组:时间复杂度O(log n)
二分查找每次只需在剩下的一半数组中查找数据,直到只剩下一个数据。
所以n/2/2/2/2.....=1,即n/2^k=1 , ( k=log以2为底,n的对数),标准化之后为O(log n)
3.插入:时间复杂度O(n)
首先记住:只要代码的执行时间不随着n的增大而增大,这样的代码时间复杂度都是O(1)
插入操作需保证数组的连续性,所以需要做到,将"插入元素"后面的数据整体后移,最坏情况下(插入开头),需要将n个数据都整体后移,而在插入操作中,数组长度越大,执行时间就越长,所以不可能是O(1),时间复杂度是O(n)。
4.删除:时间复杂度O(n)
和插入类似,只是将删除元素的后面的元素都进行前移。
如何实现数组和List之间的转换
数组转List:asList( )静态方法,必须Arrays.asList( ),是Arrays类的方法。
public class Main {
public static void main(String[] args) {
String [] a={"lj","jjj"};
List<String>list=Arrays.asList(a);阿瑞斯.矮子李
for (String s : a) {
System.out.println(s);
}
}
}
List转数组:toArray( ) toArr顾名思义,转换成数组
// 其中toArray( )将集合manlist转换为数组,
// toArray方法里放着传入的地方,是和manlist等长的String数组
import java.util.*;
public class Main {
public static void main(String[] args) {
ArrayList<String> manlist = new ArrayList<>();
Collections.addAll(manlist, "马嘉祺-24", "马嘉祺-25");
String [] arr=manlist.toArray(new String[manlist.size()]);
}
}
ArrayList:
源码分析:
ArrayList分为jdk1.8之前和jdk1.8之后。
jdk1.8之前:
如果ArrayList list=new ArrayList();//在底层将创建一个长度为10的Object[]数组elementData。
如果某次添加元素导致底层的elementData数组容量不够,则进行grow扩容。
默认扩容为原来容量的1.5倍,同时将原有数组复制到新数组。(有小数向上取整)
所以我们最好在创建时就设置容量(使用有参构造器) ArrayList list=new ArrayList(int capacity);
jdk1.8之后:
ArrayList list=new ArrayList();//Obejct[] elementData初始化为{ }
当第一次添加元素时,才创建长度为10的数组(可以有效节省空间)。
后续的扩容与jdk1.8之前相同,都是原来数组长度的1.5倍,将原数组复制到扩容的新数组。
添加数据的逻辑:
1.判断能否再存储一个数据
2.如果不够,进行grow扩容为原来长度的1.5倍
3.将添加元素放到size的位置上(添加前数组长度为size,则将新元素添加到elementData[size],索引从0开始,意思就是存放在老数组的后一位。)
4.返回添加成功的布尔值
list数组、link链表(分为单向链表和双向链表:双向链表除了节点以外,还包含指向前一个节点和后一个节点的引用)
面试题:
面试题:ArrayList list=new ArrayList(10);扩容几次?
答:没有进行扩容,new ArrayList只是实例化了一个ArrayList,并指定初始化长度为10。
面试题:使用Array.asList()将数组转为list后,修改数组,list会受影响吗?
答:会,asList会调用ArraysList来创建list,而list中的元素是数组元素的直接引用,并没有进行复制,实际指向同一地址。
面试题:使用toArray将集合转为数组后,修改list,数组会受影响吗?
答:不会,toArray的底层对数组进行了拷贝,修改list不会影响数组。
记: asList: 数组转List受影响 toArray: List转数组不受影响
面试题:ArrayList和LinkedList的区别是什么?
答:
1.底层数据结构:
ArrayList是动态的数组结构实现;LinkedList是双向链表的数据结构实现
2.操作数据效率:
有索引时,ArrayList按照索引查找的时间复杂度为O(1);LinkedList不支持索引查找。
索引未知时,ArrayList和LinkedList都需要遍历查找,时间复杂度为O(n)。
增加和删除时,ArrayList尾部的增加、删除元素时间复杂度为O(1),
ArratList其他部分的增加、删除时间复杂度为O(n)。
LinkedList头部和尾部的增加、删除元素的时间复杂度为O(1),
LinkedList其他部分的增加、删除元素的时间复杂度为O(n)。
3.内存空间占用:
ArrayList底层是数组,内存连续,节省内存。
LinkedList双向链表,需存放数据和头尾指针,更占用内存。
4.线程安全:
ArrayList和LinkedList都不是线程安全的。
Map双列集合:
HashMap1.7和1.8详细对比:
jdk1.7:数组+链表
jdk1.8:数组+链表+红黑树
初始化数组:
jdk1.7时,创建HashMap对象,会直接初始化大小为16的数组Entry。
jdk1.8以后,HashMap的构造函数仅用于初始化容量大小capacity和负载因子load factor,但其实没有初始化哈希表,等到添加第一个键值对时,才真正对哈希表进行resize()初始化。(HashMap懒加载)
如何向HashMap添加元素
1.先判断数组是否为空
(jdk1.7之前数组不为空,无需判断,数组是直接初始化长度为16的。)
jdk1.8之后,除非自定义了数组长度(自定义长度如果不是2的幂次方,则取自定义长度的下一个为2的幂次方的数组,比如自定义为7,数组将会改成8,自定义17,数组改成32),否则数组为空,当添加第一个元素时,直接调用resize()方法进行初始化
2.计算要添加数据的索引值
1)先求哈希值(hashcode方法)
2)进行扰动函数操作
3)计算索引值,(n-1)&hash值 (jdk1.8之后)
红黑树之十万个为什么:
为什么引入红黑树(一种自平衡的二叉查找树)?
因为哈希冲突比较严重时,存在链表过长的问题,将会导致元素查找较慢。
为什么链表长度大于8时会转换成红黑树?
首先因为红黑树节点占用的空间是普通链表节点的两倍,其次,哈希冲突发生8次的概率很小了,几乎是不可能事件。
为什么当红黑树的节点个数小于等于6,就会转换成链表?
因为红黑树需要旋转和变色来维持平衡性,维护成本较高。而且在节点小于等于6时,链表的遍历劣势对性能的影响小于红黑树带来的影响。所以为了优化性能,转成链表是比较合理的。
为什么条件之一是数组长度大于64转成红黑树? 哈希表容量较小就是(数组长度较小)
首先从转换的角度,转换成红黑树的成本是比较大的,就是数组中存在哈希冲突,也可以通过扩容来适当缓解,带来的影响还是会比转换成红黑树的影响要小。
为什么不直接用hashcode值来作为HashMap的索引?
hash值是从hashCode方法获取的,一个初步定位键值对在哈希表中位置的值,还需要和数组长度进行与运行,因为哈希值的范围和桶的数组范围不一致,还需要精密确定具体存放位置,因为hash值还是比较大的,和数组长度做直接索引不方便。
为什么数组容量必须是2的倍数?
为了计算索引时的效率更高
jdk1.8之后的求索引:索引位置i=(n-1) & hash值,(即n数组长度大小-1“右移1”之后的二进制和hash值的二进制进行与运算)获得索引的值的效果与jdk1.7时的hash值%n一样,但是与运算的效率比取模运算高。如果数组长度n为2的次幂,那么n-1的低位就全是1,和哈希值做与运算时,可以保证低位值不变。
为什么负载因子就取0.75?
负载因子用来表示哈希表的填满程度,负载因子越大,说明哈希表填的越满,空间利用率就高,但是哈希冲突的概率也更高;负载因子越小则相反,空间利用率小,但是哈希冲突概率也小。
扩容临界值的计算:hashMap.size>=Capacity*loadFactory,哈希表当前已使用的空间大小如果大于等于总容量*负载因子,就是已经使用容量的75%,就会触发扩容。
使用具体值0.75主要是为了提高空间利用率,并减少查询成本。
如果为1,空间利用率是高,但是哈希冲突也高,导致查询时间久,时间效率上下降。
如果为0.5,空间利用率不高,也经常发生扩容。
扰动函数
jdk1.7:四次位运算>>>,五次异或运算^
jdk1.8:一次位运算,一次异或运算(为了效率考虑,进行次数缩减)
扰动函数和哈希值二次处理的作用:
进一步提高哈希值低位的随机性,使得元素下标位置具有随机性,能够均匀分布,进而减少哈希冲突。
插入数据
jdk1.7采用头插法,将原来的数据往后移一位(多线程并发情况下,可能出现链表成环问题)
jdk1.8采用尾插法(直接插入链表尾部或者红黑树中)
HashMap线程不安全
jdk1.7,当并发执行扩容时产生环形链和数据丢失情况。
jdk1.8,当并发执行put操作时发生数据覆盖问题。
HashMap的扩容机制resize()
resize()的作用:
1.初始化数组:
jdk1.8,创建hashmap实例时,不会立即对hashmap初始化,而是当第一次有元素添加时,resize()才会根据容量来初始化数组。
2.扩容:
当元素个数达到容量*负载因子时(12=16*0.75),将会扩容至原来的2倍.
扩容后的存储位置
jdk1.7,元素索引=hash%新数组长度 是确定元素在新数组存放位置的基本方法。
如果在扩容过程中,遇到链表结构,需要对链表中的数据进行重写分配位置,因为数组中元素的位置也可能发生改变了。具体处理方法是:如果哈希值和老数组容量做与运算e.hash & oldCap==0,则存放位置保持不动;而当e.hash & oldCap!=0时,数组中的元素位置就变成:原来的位置+老容量
jdk1.8,扩容后,数组变为原来的两倍。经过一次与运算和一次异或运算的哈希值,再与新数组的长度-1做与运算
公式:(n - 1) & (h ^ (h >>> 16))就可以将哈希值转换成0到n-1的索引值。
HashMap和HashSet的区别
HashSet实现Set接口,仅存储对象;HashMap实现Map接口,存储键值对。
HashSet的底层是用HashMap实现的,封装了一系列HashMap的方法,依靠HashMap来存储元素值(利用hashMap的key键进行存储,value默认为Object对象),索引HashSet也是不重复的。
锁
悲观锁、同步锁、互斥锁、重量级锁基本同一个意思。
CAS:compare and swep 乐观锁、无锁、轻量级锁、自旋锁。
原子性:使得其他进程不会在第一个线程操作时,进来干扰操作。即第一个线程的操作是上锁的。
上下文切换就是cpu从一个线程切换到另一个线程。一个线程的时间片用完,操作系统就会暂停这个线程的执行,保存它的上下文,再将cpu分配给另一个等待的线程。
地基:互斥锁(互斥锁加锁失败后,线程释放 cpu,进行线程切换, 给其他线程)和自旋锁(通过 cpu 提供的 CAS 函数,在用户态进行加锁和解锁,不主动进行上下文切换,比互斥锁更快,开销也更小)(自旋锁加锁失败后,会忙等待(while 循环),直到他拿到锁)。
上下文切换就是共享的虚拟资源不做改变,只切换线程的不共享数据。
乐观锁
也叫无锁编程,去除了加锁解锁的操作,他认为线程冲突概率很低,于是先修改共享资源,再验证这段时间是否发生了冲突,如果没有其他线程修改,则操作完成。如果有线程修改资源,则放弃本次操作。(乐观锁例子:在线编辑共享文档,提交修改后的文档,如果前后的版本号不同,证明发生了冲突,就重新进行修改提交。当版本号一样才能上传成功)(乐观锁适用于发生冲突概率很低,加锁成本很高的场景)
悲观锁:
认为发生进程冲突的概率很高,于是访问共享资源之前,就先进行上锁
读写锁:
适用于有明显读操作和写操作的场景。
读锁可以多个线程同时拥有,因为读操作不会对共享资源做出改变。但是写锁被一个线程获取后,其他读进程或写线程都不能拥有读锁或者写锁,因为发生了阻塞。因此,写锁是一种独占锁,读锁是共享锁。
后续又区分了读优先锁、写优先锁。读优先锁希望有更多的线程进行读操作,提供并发性;写优先锁希望优先服务于写线程。写优先锁为了抢到锁,先进入阻塞状态,防止后续的读操作更快获取锁,先进入阻塞导致读进程不会优先获取锁,而是和写线程一起阻塞,等最初的读线程结束后,写线程就直接上位。但是一直有写优先锁,就会导致读线程被饿死;反之一直有读优先锁也会导致写线程被饿死。于是推出公平读写锁,将读写线程都存放在队列中,按照先进先出的原则,保证并发的同时,也不会出现任何的饿死现象。
偏向锁:
一个线程获得了锁,就进行偏向模式,下次这个线程再对锁进行申请,就不用再做任何同步操作了,锁对这个线程偏心,这样就节省了锁申请操作。
偏向锁主要用于解决读写锁的性能问题。
读写锁需要不断进行加锁解锁,消耗很多 cpu 时间,偏向锁采用乐观锁的机制。在读线程进行时,如果没有写线程操作,就不用对读线程进行加锁解锁操作,直接进行共享内存的访问。而当有写线程操作时,就进行自旋等待(while())
轻量级锁:
在偏向锁中,当一个线程进入同步块后,有其他线程来抢锁时,偏向锁就会升级成轻量级锁。
轻量级锁说明,虽然同步代码块中有多个线程,但是没有发生同时访问,不需要上升到操作系统的阻塞情况。
自旋锁:
概念:在锁膨胀之后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力——自旋锁。
JVM会进行一次赌注:它会假设在不久的将来,线程可以得到这把锁。因此,虚拟机会让当前线程做几个空循环(这也是自旋的含义,也叫cpu空转),在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真正地将线程在操作系统层面挂起,升级为重量级锁。
(不太理解)
锁消除:
虚拟机在编译时,对运行上下文扫描,去除不可能存在共享资源竞争的锁。
重量级锁:
重量级锁(Heavyweight Lock)是一种在操作系统级别提供的锁机制,它在获取和释放时对系统资源有较大的占用。当线程尝试获取这种锁时,会进入阻塞状态,直到锁被释放。在Java中,传统的synchronized关键字就属于重量级锁,比如在高并发场景下,如果大量线程争抢同步代码块,可能会导致CPU上下文切换频繁,性能下降,这就是“锁膨胀”现象。
相比之下,轻量级锁(Lightweight Lock)如Java 8引入的ReentrantLock,虽然底层也是基于CAS操作,但在实现上更为精细,不会导致线程长时间阻塞,因此更适合于高并发环境。当线程竞争较小时,轻量级锁可以表现为无锁粒度,效率更高。