参考1:java之封装,继承,多态
推荐看参考1。
private
修饰符,表示最小的访问权限。对成员变量的访问,统一提供setXXX
,getXXX
方法。封装的特点
特征
和行为
,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。但是如果在父类中拥有私有
属性(private
修饰),则子类是不能被继承的。继承的特点
多态的特点
多态的体现形式
参考1:java之封装,继承,多态
this
关键字用法:
1. 本类成员方法中,访问本类的成员变量。
2. 本类成员方法中,访问本类的另一个成员方法。
3. 本类的构造方法中,访问本类的另一个构造方法。
super
()关键字的用法
1. 子类的成员方法中,访问父类的成员变量。
2. 子类的成员方法中,访问父类的成员方法。
3. 子类的构造方法中,访问父类的构造方法。
注意:
this
关键字同super
一样,必须在构造方法的第一个语句,且是唯一的。
this
与super
不能同时存在。
参考1:java基本数据类型
java语言中有8种基本数据类型,分类四大类型:
记忆规律:byte、short、char、int、long、float、double(占用内存分别是:1、2、2、4、8、4、8)
参考1:Java中public、private、default和protected详解
public
修饰的变量,当前类、当前包、子类和其他类均可访问。protected
修饰的变量,可在当前类访问,也可在当前包和子类(继承父类的子类)中访问。private
修饰词修饰的变量,该变量仅能在当前类内访问,其他地方(如:当前包、子类、其他类)均无法访问。参考1:Java 接口
Java的接口是一个抽象类型,是抽象方法的集合,用interface
声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性
和方法
。接口则包含类要实现的方法
。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。
接口的特点:
static
和 final
变量。参考1:Java 抽象类
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量
、成员方法
和构造方法
的访问方式和普通类一样。
抽象类的特点:
static
修饰的方法)不能声明为抽象方法。参考1:Java 接口
public
static
final
类型的。static
修饰的方法),而抽象类是可以有静态代码块和静态方法。参考1:java之封装,继承,多态
重载(overload)
是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。
重载规则:
重写(override)
是子类对父类允许访问的方法的实现过程进行重新实现, 返回值和形参都不能改变。
重写规则:
public>protected>(default)>private
)。final
修饰的方法不能被重写。final
是一个修饰符,可以修饰变量、方法和类。如果 final
修饰变量,意味着该变量的值在初始化后不能被改变。
finalize
是方法,它是在对象被回收之前调用的方法,给对象自己最后一个复活的机会,但是什么时候调用 finalize
没有保证。
finally
是一个关键字,与 try
和 catch
一起用于异常的处理。finally
块一定会被执行,无论在 try
块中是否有发生异常。
final
修饰的类不可以被继承。final
修饰的方法不可以被重写。final
修饰的变量不可以被改变。如果修饰引用,那么表示引用不可变,引用指向的内容可变。final
修饰的方法,JVM
会尝试将其内联,以提高运行效率。final
修饰的常量,在编译阶段会存入常量池中。浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
static
关键字这两个基本的用法:静态变量
和静态方法
。也就是被static
所修饰的变量/方法都属于类的静态资源,类实例所共享。
除了静态变量和静态方法之外,static
也用于静态块,多用于初始化操作。
此外static
也多用于修饰内部类,此时称之为静态内部类。
注意:
static
修饰的静态方法不能直接调用非静态的方法,需要先创建类的实例再调用。static
修饰的静态方法。Integer
是int
的包装类型,在拆箱和装箱中,二者自动转换。
int
是基本类型,直接存数值,而Integer
是对象,用一个引用指向这个对象。
String:是字符串常量,不可变,原因是有final
修饰;
StringBuffer:字符串变量(线程安全),StringBuffer
类里很多方法都有 synchronized
(同步锁)实现线程安全;
StringBuilder:字符串变量(线程不安全)。
String
和StringBuffer
主要区别是性能:String
是不可变对象,每次对String
类型进行操作都等同于产生了一个新的String
对象,然后指向新的String
对象。所以在使用时尽量不要对String
进行大量的拼接操作,否则会产生很多临时对象,导致GC
开始工作,影响系统性能。
StringBuffer
是对对象本身操作,而不是产生新的对象,因此在有大量拼接的情况下,建议使用StringBuffer
。
StringBuffer
和StringBuilder
都是 extends AbstractStringBuilder implements java.io.Serializable, CharSequence
。
StringBuffer
是线程安全的可变字符串,其内部实现是可变数组。
StringBuilder
是jdk 1.5
新增的,其功能和StringBuffer
类似,但是非线程安全。因此,在没有多线程问题的前提下,使用StringBuilder
会取得更好的性能。
参考1:Java 垃圾回收机制整理
参考2:Java超详细分析垃圾回收机制
参考3:深入理解 Java 垃圾回收机制
垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存;
内存泄露:指该内存空间使用完毕后未回收,在不涉及复杂数据结构的一般情况下,java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有是也将其称为 “对象游离
”;
参考1:JAVA垃圾回收机制
当没有线程在运行时,垃圾回收会被调用。因为垃圾回收在优先级最低的线程中进行,当应用忙时,垃圾回收不被调用(不是由程序员自己调用的),但除堆内存不足外。
堆内存不足时会触发垃圾回收机制。
参考1:面试必问:Java 垃圾回收机制
System.gc()
方法:系统类包含静态方法gc()
用于请求 JVM 运行垃圾收集器。Runtime.getRuntime().gc()
方法:运行时类允许应用程序与运行应用程序的 JVM
交互。因此,通过使用其 gc()
方法,我们可以请求 JVM
运行垃圾收集器。注意:以上两种方法中的任何一种都不能保证一定会运行垃圾收集器,也就是系统是否进行垃圾回收依旧不确定,因为这不是程序员控制的,而是系统决定的。
标记阶段有:1、引用计数法(Reference Counting Collector);2、根搜索算法(可达性检测算法)。
引用计数是垃圾收集器中的早期策略。
这个方法中,堆中的每个对象都会添加一个引用计数器。每当一个地方引用这个对象时,计数器值 +1
;当引用失效时,计数器值 -1
。任何时刻计数值为 0
的对象就是不可能再被使用的。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法解决对象之间相互引用的情况。比如对象有一个对子对象的引用,子对象反过来引用父对象,它们的引用计数永远不可能为 0。
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。所以现在一般使用根搜索算法。
根搜索算法是从离散数学中的图论
引入的,程序把所有的引用关系看作一张图,从一个节点 GC ROOT
开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
如上图中的 ObjF
、ObjD
、ObjE
通过 GC Root
是无法找到的,所以它们是无用节点。
java中可作为GC Root
的对象:
Native
对象)。标记-清除算法分为两个阶段:
为了解决标记 - 清除
算法在效率方面的缺陷,复制算法采用将内存按容量划分的方式,划分成大小相等的两块,每次只使用其中的一块。算法思想如下 (1或2是一个意思):
将正在使用的存活对象全部复制到另一块未被使用空间,摆放整齐,然后清空此空间所有对象。
当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。
优点:实现简单,不易产生内存碎片,每次只需要对半个区进行内存回收。
缺点1:内存空间缩减为原来的一半;算法的效率和存活对象的数目有关,存活对象越多,效率越低。
缺点2 : 需要两倍的内存空间 , 开销较大 , 另外GC如果采用 G1 垃圾回收器的话 , 它将空间拆成了很多份, 如果采用复制算法, 还需要维护各区之间的关系。
为了更充分利用内存空间,提出了标记-压缩算法。此算法结合了“标记-清除”和“复制”两个算法的优点。也是分为两个阶段:
标记-清除-压缩(Mark-Sweep-Compact)算法
, 标记- 压缩是移动式的 , 将对象在内存中依次排列比维护一个空列表少了不少开销(如果对象排列整齐,当我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可)。优点 : 相对于标记 -清除算法避免了内存碎片化,相对于复制算法,避免开辟额外的空间。
缺点 : 从效率上来说是不如复制算法的,移动时,如果存在对象相互引用, 则需要调整引用的位置, 另外移动过程中也会有STW。
复制算法是效率最高的 , 但是花费空间最大。
标记 - 压缩算法虽然较为兼顾 ,但效率也变低,比标记- 清除多了个整理内存的过程,比复制算法多了标记的过程。
分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。
分代的垃圾回收策略,是基于:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)
和新生代(Young Generation)
,老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
年轻代(Young Generation)
eden
区和两个 survivor
(survivor0
,survivor1
) 区。一个 Eden
区,两个 Survivor
区(一般而言)。大部分对象在 Eden
区中生成。回收时先将 eden
区存活对象复制到一个 survivor0
区,然后清空 eden
区,当这个 survivor0
区也存放满了时,则将 eden
区和 survivor0
区存活对象复制到另一个 survivor1
区,然后清空 eden
和这个 survivor0
区,此时 survivor0
区是空的,然后将 survivor0
区和 survivor1
区交换,即保持 survivor1
区为空, 如此往复。survivor1
区不足以存放 eden
和 survivor0
的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次 Full GC
,也就是新生代、老年代都进行回收。GC
也叫做 Minor GC
,Minor GC
发生频率比较高(不一定等 Eden
区满了才触发)。年老代(Old Generation)
N
次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。Major GC
即 Full GC
,Full GC
发生频率比较低,老年代对象存活时间比较长,存活率标记高。持久代(Permanent Generation)
用于存放静态文件,如 Java
类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class
,例如 Hibernate
等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。
GC
,指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生熄灭的特点,所以 Minor GC
十分频繁,回收速度也较快。GC
,指发生在老年代的垃圾收集动作,当出现 Major GC
时,一般也会伴有至少一次的 Minor GC
(并非绝对,例如 Parallel Scavenge
收集器会单独直接触发 Major GC
的机制)。 Major GC
的速度一般会比 Minor GC
慢十倍以上。产生 Full GC
可能的原因:
GC
之后 Heap(堆)
的各域分配策略动态变化。不同虚拟机所提供的垃圾收集器可能会有很大差别,下面的例子是 HotSpot
。
Serial
收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效。Serial Old
收集器(标记-整理算法):老年代单线程收集器,Serial
收集器的老年代版本。ParNew
收集器(停止-复制算法) :新生代收集器,可以认为是 Serial
收集器的多线程版本,在多核 CPU
环境下有着比 Serial
更好的表现。Parallel Scavenge
收集器(停止-复制算法):并行收集器,追求高吞吐量,高效利用 CPU
。吞吐量一般为 99%
, 吞吐量 = 用户线程时间 / (用户线程时间 + GC线程时间)。适合后台应用等对交互相应要求不高的场景。Parallel Old
收集器(停止-复制算法):Parallel Scavenge
收集器的老年代版本,并行收集器,吞吐量优先。CMS(Concurrent Mark Sweep)
收集器(标记-清理算法):高并发、低停顿,追求最短 GC
回收停顿时间,cpu
占用比较高,响应时间快,停顿时间短,多核 cpu
追求高响应时间的选择。总结:
根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。
在实际开发中,我们对 new 出来的对象也会根据重要程度,有个等级划分。有些必须用到的对象,我们希望它在其被引用的周期内能一直存在;有些对象可能没那么重要,当内存空间还足够时,可以保留在内存中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。
由此,Java 对引用划分为四种:强引用、软引用、弱引用、虚引用,四种引用强度依次减弱。
Object obj = new Object()
"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。Java
中的类 SoftReference
表示软引用。Java
中的类 WeakReference
表示弱引用。Java
中的类 PhantomReference
表示虚引用。参考1:java类加载机制,你会了吗?
类加载机制定义:
java
虚拟机将编译后的class
文件加载到内存中,进行校验、转换、解析和初始化,到最终的使用。这就是java
类加载机制;
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)等阶段,其中验证、准备、解析3阶段也可以称为连接(Lingking),如下图(类的生命周期):
在类加载的生命周期中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,其加载过程一定是按照这个顺序执行的。而解析阶段有点特殊,在某些特定的情况下,它是在初始化之后开始的。
Java虚拟机规范中并没有进行强制约束类加载的第一个阶段的时机,而是交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况(类没有初始化)必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
new
、getstatic
、putstatic
或invokestaic
这四条字节码指令时。java.lang.reflect
包的方法对类进行反射调用的时候。JDK1.7
的动态语言支持的时候,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个句柄所对应的类没有进行过初始化,则需要先触发器初始化。1 加载:在加载阶段,虚拟机主要执行以下三个操作。
这个阶段相比其他阶段来说,是开发人员可控性最强的阶段。因为这个阶段既能使用系统提供的加载器(这个加载器后面会进行介绍)加载,又能通过开发人员自定义的加载器进行加载。
在加载这个阶段还有一个需要注意的地方,在执行第一个操作时,需要知道可以从哪里获取class文件,例如:
2 验证:验证阶段主要有4个阶段的验证:文件格式验证、元数据验证、字节码验证和符号验证。
Class
文件格式的规范,并且能被当前版本的虚拟机处理,主要包括魔数、版本号、常量池等验证。java
语言规范的要求。主要包括是否有父类,类中的字段、方法是否与父类冲突,如果不是抽象类,是否实现了其父类或接口中要求实现的所有方法等;3 准备:准备阶段是为类变量分配内存并设置类变量初始值的阶段,分配这些内存是在方法区里面进行的,这个阶段有两点需要重点注意的:
public static int number=111
,这类变量number
在准备阶段之后的初始值是0而不是111
。而给number
赋值为111
是在初始化阶段。4 解析:解析阶段是虚拟机将常量池里内的符号引用转换为直接引用。注意2个概念:
解析操作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用进行的。
5 初始化:这个阶段是类加载过程的最后一步,是代码真正开始执行的时候,在这个阶段,开发人员可以根据自己的需求去给类变量初始化赋值。简单来说就是执行类构造器()方法的过程。
接下来看看是什么是类加载器:
虚拟机设计团队将加载动作放到了Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为“类加载器”。
启动类加载器(Bootstrap ClassLoader):负责将存放在
目录中,或者被-Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。(注:仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)
扩展类加载器(Extension ClassLoader):负责加载
目录中的,或被java.ext.dirs
系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):负责加载用户路径**(ClassPath)**上所指定的类库,开发者可以直接使用这个类加载器,一般情况下该类加载是程序中默认的类加载器。
这三种加载器的加载顺序如下:(系统提供的类加载器的执行顺序)
如上图展示的类加载器之间的这种层次关系就是双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应有自己的父类加载器。
双亲委派介绍
如果一个类加载器收到类加载的请求,他首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一层次的类加载器都是这样,因此所有的加载请求最终都应该传送到底层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试去加载。
双亲委派原则的好处
上面讲述的是系统提供的类加载器以及它们之间的关系,还有很多情况需要我们自定义类加载器。那该如何定义呢?有以下两种方式:
java.lang.ClassLoader
类并重写 findClass
方法。java.lang.ClassLoader
类并重写loadClass(java.lang.String)
方法。参考1:Java的异常处理机制
参考2:Java:详解Java中的异常(Error与Exception)
Throwable
。其下有两个子类:Error
和 Exception
。Error
的子类描述的都是系统错误,比如虚拟机内存溢出。Exception
的子类描述的都是程序,比如空指针,下标越界等。Exception
。Error:是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM
(Java
虚拟机)出现的问题。例如,Java
虚拟机运行错误(Virtual MachineError
),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError
。这些异常发生时,Java
虚拟机(JVM
)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError
)、类定义错误(NoClassDefFoundError
)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java
中,错误通过Error
的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception(异常)分两大类:运行时异常
和非运行时异常(编译异常)
。程序中应当尽可能去处理这些异常。
运行时异常:都是RuntimeException
类及其子类异常,如NullPointerException(空指针异常)
、ArrayIndexOutOfBoundException(数组下标越界异常)
、ArithmeticException(算术异常)
、NullPointerException(空指针异常)
、NumberFormatException(数字格式异常)
等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch
语句捕获它,也没有用throws
子句声明抛出它,也会编译通过。
非运行时异常 (编译异常):是RuntimeException
以外的异常,类型上都属于Exception
类及其子类。这类是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException
、SQLException
等以及用户自定义的Exception
异常,一般情况下不自定义检查异常。
Java的异常(Throwable)分为可查的异常(checked exceptions)
和不可查的异常(unchecked exceptions)
。
可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。除了Exception
中的RuntimeException
及RuntimeException
的子类以外,其他的Exception
类及其子类(例如:IOException
和ClassNotFoundException
)都属于可查异常。这种异常的特点是Java
编译器会检查它,当程序中可能出现这类异常,要么用try-catch
语句捕获它,要么用throws
子句声明抛出它,否则编译不会通过。
不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException
与其子类)和错误(Error
)。RuntimeException
表示编译器不会检查程序是否对RuntimeException
作了处理,在程序中不必捕获RuntimException
类型的异常,也不必在方法体声明抛出RuntimeException
类。RuntimeException
发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException
。
RuntimeException
类属于非检测异常,因为普通JVM
操作引起的运行时异常随时可能发生,此类异常一般是由特定操作引发。但这些操作在java应用程序中会频繁出现。因此它们不受编译器检查与处理或声明规则的限制。
在Java
应用程序中,异常处理机制为:异常抛出,异常捕获。
异常抛出:任何Java
代码都可以抛出异常,如:自己编写的代码、来自Java
开发环境包中代码,或者Java
运行时系统。无论是谁,都可以通过Java
的throw
语句抛出异常。从方法中抛出的任何异常都必须使用throws
子句。
异常捕获:捕捉异常通过try-catch语句或者try-catch-finally语句实现。
finally
块定义在异常处理机制中的最后一块。它可以直接跟在try
之后,或者最后一个catch
之后。finally
可以保证只要程序执行到了try
语句块中,无论try
语句块中的代码是否出现异常,最终finally
都必定执行。finally
通常用来做释放资源这类操作。参考1:Java中throw和throws有什么区别?
throw
作用在方法体内使用,throws
在方法声明上使用;throw
后面接的是异常对象,只能接一个。throws
后面接的是异常类型,可以接多个,多个异常类型用逗号隔开;throw
是在方法中出现不正确情况时,手动来抛出异常,结束方法的,执行了 throw
语句一定会出现异常。而 throws
是用来声明当前方法有可能会出现某种异常的,如果出现了相应的异常,将由调用者来处理,声明了异常不一定会出现异常。当Java内置的异常都不能明确的说明异常情况的时候,需要创建自己的异常。
定义自定义异常需要注意以下问题:
参考1:Java泛型详解,史上最全图文详解
Java
泛型(generics)是 JDK5
中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类
、泛型接口
、泛型方法
。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
泛型的好处
Object
传递时会引起Boxing
(装箱)和Unboxing
(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing
和Unboxing
操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。泛型有三种使用方式,分别为:泛型类、泛型接口和泛型方法。
泛型类
:把泛型定义在类上。泛型接口
:把泛型定义在接口上。泛型方法
:把泛型定义在方法上。Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:
无边界的通配符(Unbounded Wildcards)
,就是>
,比如List>
。
无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。
固定上边界的通配符(Upper Bounded Wildcards),采用 extends E>的形式
(1)使用固定上边界的通配符的泛型,就能够接受指定类及其子类类型的数据。
(2)要声明使用该类通配符,采用 extends E>
的形式,这里的E就是该泛型的上边界。
注意:这里虽然用的是extends
关键字,却不仅限于继承了父类E的子类,也可以代指显现了接口E的类。
固定下边界的通配符(Lower Bounded Wildcards),采用 super E>的形式
(1)使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据.。
(2)要声明使用该类通配符,采用 super E>
的形式,这里的E就是该泛型的下边界。
注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界。
上面这些泛型类定义中的泛型参数E、K和V都是什么意思呢?其实这些参数名称是可以任意指定,就想方法的参数名一样可以任意指定,但是我们通常会起一个有意义的名称,让别人一看就知道是什么意思。泛型参数也一样,E一般是指元素,用来集合类中。
常见泛型参数名称有如下:
E: Element (在集合中使用,因为集合中存放的是元素)
T:Type(Java 类)
K: Key(键)
V: Value(值)
N: Number(数值类型)
?: 表示不确定的java类型
泛型本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。
参考1:Java反射(通俗易懂)
Reflection
(反射) 允许运行中的 Java 程序对自身进行检查。被private
封装的资源只能类内部访问,外部是不行的,但反射能直接操作类私有属性。反射可以在运行时获取一个类的所有信息,(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。
要想查看一个类的具体信息,必须先要获取到该类的字节码文件对象。而查看一个类的具体信息使用的就是Class类中的方法。所以先要获取到每一个字节码文件对应的Class类型的对象。
反射就是把java
类中的各种成分映射成一个个的Java
对象。例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把一个个组成部分映射成一个个对象。(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述)
加载的时候:Class
对象的由来是将 .class
文件读入内存,并为之创建一个Class
对象。
Class类
Class
类的实例表示正在运行的 Java
应用程序中的类和接口。也就是jvm
中有N多的实例每个类都有该Class
对象。(包括基本数据类型)
Class
没有公共构造方法。Class
对象是在加载类时由 Java
虚拟机以及通过调用类加载器中的defineClass
方法自动构造的。也就是这不需要我们自己去处理创建,JVM
已经帮我们创建好了。
Bean
Spring
框架可以帮我们创建和管理对象。需要对象时,我们无需自己手动new
对象,直接从Spring
提供的容器中的Beans
获取即可。Beans
底层其实就是一个Map
,最终通过getBean(“user”)
来获取。而这其中最核心的实现就是利用反射技术。
Java
面向对象,对象有方法和属性,那么就需要对象实例来调用方法和属性(即实例化);Bean
到Spring IoC
的注解;(@Component
, @Repository
,@ Controller
,@Service
, @Configration
)。看 Java反射(通俗易懂) 3、反射的应用
参考1:java的几种IO
参考2:网络IO模型(BIO,NIO,AIO)
参考1主要是概念,参考2有更深入的分析及代码示例。
Java IO
基于不同的IO
模型可以分为三类:
BIO(blocking IO)
,在jdk1.0
的时候引入的。NIO(non-blocking IO)
,因为是在jdk1.4
引入的,所以又叫(New IO)
;AIO(Asynchronous IO)
,是在jdk1.7
引入的,是对NIO
进一步的改进,也被称为NIO2
。IO
又主要可以分为文件IO
和网络IO
。
一般常说的java中的IO流指的就是java中BIO的具体实现
同步:
就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。(当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步)
**异步:**是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。(其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。)
异步和同步是对于请求结果的获取是客户端主动等待获取,还是由服务端来通知消息结果。
阻塞:
阻塞与非阻塞主要是从 CPU
的消耗上来说的,阻塞就是 CPU
停下来线程挂起等待当前一个慢的操作完成 CPU
才接着完成其它的事。阻塞期间无法从事其他任务,只有当慢操作完成条件就绪才能继续。
非阻塞:
非阻塞就是在这个慢的操作在执行时 CPU
去干其它别的事,等这个慢的操作完成时,CPU
再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU
的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU
使用时间能不能补偿系统的切换成本需要好好评估。
阻塞和非阻塞通常是指客户端在发出请求后,在服务端处理这个请求的过程中,客户端本身是否直接挂起等待结果,还是继续做其他的任务。
首先,传统的java.io
包是 blocking io(BIO)
,在jdk1.0
的时候引入的,它提供了我们最熟知的一些IO
功能,比如File
抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是在读入输入流或者写入输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。
优点:代码比较简单、直观;
缺点:是IO
效率和扩展性存在局限性,容易成为应用性能的瓶颈。
同步阻塞(BIO)的特点:
BIO
不仅仅是对文件的操作,网络编程中,比如Socket
通信,都是典型的BIO
操作目标。InputStream
/OutputStream
)是用于读取或写入字节的,例如操作图片文件。Reader
/Writer
则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader
/Writer
相当于构建了应用逻辑和原始数据之间的桥梁。字符流(xxxReader/xxxWriter)
,处理非纯文本时使用字节流(xxxStream)
。最后其实不管什么类型文件都可以用字节流处理,包括纯文本,但会增加一些额外的工作量。所以还是按原则选择最合适的流来处理。BuferedOutputStream
等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高IO
处理效率。这种设计利用了缓冲区,将批量数据进行一次操作,但在使用中千万别忘了结束时调用fush
将未满的缓冲区数据进行写入。流是Java对不同输入源输出源的抽象,代表了从起源到接收的有序数据,有了它程序就可以采用统一的方式来访问不同的输入源和输出源了。
byte
(8位的字节),而字符流操作的数据单元是char
(16位的字符)。NIO
是 Java 1.4
引入的 java.nio
包,提供了 Channel
(通道)、Buffer
(缓冲区)、Selector
(选择器) 等新的抽象,可以构建多路复用的、同步非阻塞 IO
程序,同时提供了更接近操作系统底层高性能的数据操作方式。
在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)。
传统IO
基于字节流和字符流进行操作,而NIO
基于Channel
(通道)和Buffer
(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector
(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
BIO
的各种流是阻塞的。所以当一个线程调用read()
或 write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
NIO
的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
NIO
的线程通常将非阻塞IO
的空闲时间用于在其它通道上执行IO
操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)
。
Channel(通道)
Channel
是一个通道,可以通过它读取和写入数据。与流不同的是,流是单向的,而Channel
是双向的。数据可以通过Channel
读到Buffer
里,也可以通过Channel
写入到Buffer
里。需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。Channel
接口有好几种子类,如FileChannel
用于访问磁盘文件、SocketChannel
和ServerSocketChannel
用于TCP协议的网络通信、DatagramChannel
用于UDP协议的网络通信。Buffer(缓冲区):
NIO
是面向缓冲区的,在NIO
中所有的数据都是通过缓冲区处理的。Buffer
就是缓冲区对象,无论读取还是写入,数据都是先进入Buffer
的。Buffer
的本质是一个数组,通常它是一个字节数组,也可以是其他类型的数组。Buffer
是一个接口,它的实现类有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer
,分别对应基本数据类型: byte、char、double、float、int、long、short
。Buffer
对象包含三个重要的属性,分别是capacity
、position
、limit
。capacity
:代表Buffer
的容量,就是说Buffer
中最多只能写入capacity
个数据。position
:代表访问的位置,它的初始值为0
,每读取/写入一个数据,它就会向后移动一个位置。limit
:代表访问限制,就是本次操作最多能读取/写入多少个数据。这三个属性的关系是,position<=limit<=capacity
,Buffer
通过不断调整position
和limit
的值,使得自身可以不断复用。Selector(选择器):
Selector
是多路复用器,可以通过它监听网络IO
的状态。它可以不断轮询注册的Channel
,如果某Channel上有连接、读取、写入事件发生,则这个Channel
就处于就绪状态,就会被Selector
轮询出来。所有被轮询出来的Channel集合,我们可以通过SelectionKey
获取到,然后进行后续的IO
操作。
NIO
是以块的方式处理数据,但是IO
是以最基础的字节流的形式去写入和读出的。所以在效率上的话,肯定是NIO
效率比IO
效率会高出很多。NIO
不在是和IO
一样用OutputStream
和InputStream
输入流的形式来进行处理数据的,但是又是基于这种流的形式,而是采用了通道和缓冲区的形式来进行处理数据的。NIO
的通道是可以双向的,但是IO
中的流只能是单向的。NIO
的缓冲区(其实也就是一个字节数组)还可以进行分片,可以建立只读缓冲区、直接缓冲区和间接缓冲区,只读缓冲区很明显就是字面意思,直接缓冲区是为加快 I/O
速度,而以一种特殊的方式分配其内存的缓冲区。NIO
与BIO
核心区别就是NIO
采用的是多路复用的IO
模型,普通的IO
用的是阻塞的IO
模型,两个之间的效率肯定是多路复用效率更高。AIO
是 Java 1.7
之后引入的包,是对NIO
进一步的改进,也被称为NIO2
,提供了异步非堵塞的 IO
操作方式,所以叫 AIO(Asynchronous IO)
。AIO其实是一种在读写操作结束之前允许进行其他操作的I/O
处理。
jdk7主要增加了三个新的异步通道:
AsynchronousFileChannel
: 用于文件异步读写;AsynchronousSocketChannel
: 客户端异步socket
;AsynchronousServerSocketChannel
: 服务器异步socket
。因为AIO
的实施需充分调用操作系统参与,IO需要操作系统支持、并发也同样需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。
BIO
方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4
以前的唯一选择,但程序简单易理解。
NIO
方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4
开始支持。
AIO
方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作参与并发操作,编程比较复杂,JDK7
开始支持。
参考1:Java多线程(超详细!)
参考2:Java多线程【三种实现方法】
参考3:Java多线程详解——一篇文章搞懂Java多线程
参考4:Java多线程超级详解(看这篇就足够了)
参考5:Java多线程实战精讲-带你一次搞明白Java多线程高并发
参考6:java多线程(详)
参考1:Java中的栈和堆
Java程序在运行时都会开辟内存空间,而栈和堆就是JVM虚拟机在运行时开辟的内存空间。
栈:函数中定义的基本数据类型
与局部变量
都是在栈内存中被分配,而用完之后这些变量将会在栈内存中被释放,局部变量存在于方法中,一旦方法被执行,局部变量也将会被执行,而方法执行完后,这个变量将会在栈内存中被释放掉。
堆:实体对象(被new
出来的)在堆内存中被分配,而这些实体封装的数据属性如果在用完后被释放实体对象也不会被释放,但是Java
中垃圾回收机制,在对象不被使用后,会被自动当成垃圾不定时的回收。
示例:
int[] a = new int[2];
这里首先在栈中给a变量分配地址,在堆中给数组分配2个大小的内存空间,并给数组中的每个默认值一个地址(默认为0),这个也就是两个0,之后栈中的a会根据这个地址被赋值,当用完后,a变量会被释放掉,而new对象在堆中存在。
参考1:史上最全Java面试题(带全部答案)
参考2:100+道Java经典面试题(面中率高)
参考3:Java面试题大全带答案
参考4:最全java面试题及答案(208道)
参考5:Java 教程
可以创建一个包含可变对象的不可变对象的,你只需要谨慎一点,不要共享可变对象的引用就可以了,如果需要变化时,就返回原对象的一个拷贝。最常见的例子就是对象中包含一个日期对象的引用。
new
clone
equals()
clone()
getClass()
notify(),notifyAll(),wait()
toString
强引用,软引用,弱引用,虚引用。不同的引用类型主要体现在GC上:
不像C语言,我们可以控制内存的申请和释放,在Java中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对GC回收时机不可控的妥协。有以下几个使用场景可以充分的说明:
利用软引用和弱引用解决OOM问题:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题.
通过软引用实现Java对象的高速缓存:比如我们创建了一Person的类,如果每次需要查询一个人的信息,哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这将引起大量Person对象的消耗,并且由于这些对象的生命周期相对较短,会引起多次GC影响性能。此时,通过软引用和 HashMap 的结合可以构建高速缓存,提供性能。
==
是运算符,用于比较两个变量是否相等,而equals
是Object
类的方法,用于比较两个对象是否相等。默认Object
类的equals
方法是比较两个对象的地址,此时和 ==
的结果一样。换句话说:基本类型比较用==
,比较的是他们的值。默认下,对象用==
比较时,比较的是内存地址,如果需要比较对象内容,需要重写equal
方法。
hashCode()
是Object
类的一个方法,返回一个哈希值。如果两个对象根据equal()
方法比较相等,那么调用这两个对象中任意一个对象的hashCode()
方法必须产生相同的哈希值。
如果两个对象根据eqaul()
方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的。
hashCode()
方法是相应对象整型的 hash
值。它常用于基于 hash
的集合类,如 Hashtable
、HashMap
、LinkedHashMap
等等。它与 equals()
方法关系特别紧密。根据 Java 规范,使用 equal()
方法来判断两个相等的对象,必须具有相同的 hashcode
。
将对象放入到集合中时,首先判断要放入对象的hashcode
是否已经在集合中存在,不存在则直接放入集合。如果hashcode
相等,然后通过equal()
方法判断要放入对象与集合中的任意对象是否相等:如果equal()
判断不相等,直接将该元素放入集合中,否则不放入。
有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap
中会有冲突。如果两个对象相等,必须有相同的hashcode
值,反之不成立。
如果a 和b 都是对象,则 a==b 是比较两个对象的引用,只有当 a 和 b 指向的是堆中的同一个对象才会返回 true,而 a.equals(b)
是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String
类重写 equals()
方法,所以可以用于两个不同对象,但是包含的字母相同的比较。
+=操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换。
有错误,short
类型在进行运算时会自动提升为int
类型,也就是说s1+1
的运算结果是int
类型。
+=
操作符会自动对右边的表达式结果强转匹配左边的数据类型,所以没错。
&
是位操作,而&&
是逻辑运算符。另外需要记住逻辑运算符具有短路
特性,而&
不具备短路特性。
内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立.在单个外围类当中,可以让多个内部类以不同的方式实现同一接口,或者继承同一个类.创建内部类对象的时刻不依赖于外部类对象的创建。内部类并没有令人疑惑的”is-a”管系,它就像是一个独立的实体。
内部类提供了更好的封装,除了该外围类,其他类都不能访问。
可以使用 String 接收 byte[] 参数的构造器来进行转换,需要注意的点是要使用的正确的编码,否则会使用平台默认编码,这个编码可能跟原来的编码相同,也可能不同。
我们可以做强制转换,但是Java中int是32位的而byte是8 位的,所以,如果强制转化int类型的高24位将会被丢弃,byte 类型的范围是从-128到128。导致精度损失。
参考1:线程和进程的区别
进程:
线程:
进程和线程的关系:
参考1:java 循环依赖_Java详解之Spring Bean的循环依赖解决方案【推荐】
参考2:Java中的Spring循环依赖详情
什么是循环依赖
循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。
Spring中循环依赖场景有:
怎么检测是否存在循环依赖
检测循环依赖相对比较容易,Bean
在创建的时候可以给该Bean
打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。
运行之后,Spring抛出了如下错误信息:
Description:
The dependencies of some of the beans in the application context form a cycle:
解决方案
不使用基于构造函数的依赖注入。可通过下面方式解决。
Autowired
注解,让Spring决定在合适的时机注入。【推荐】setter
方法的依赖注射取代基于构造函数的依赖注入来解决循环依赖。