赞
踩
线程不安全的 | 线程安全的 |
---|---|
StringBuilder | StringBuffer |
ArrayList | Vector |
HashMap | Hashtable |
基本数据类型
字符型:char
布尔型:Boolean
数值型:
byte:8位,最大存储数据量是255,存放的数据范围是-128~127之间。
short:16位,最大数据存储量是65536,数据范围是-32768~32767之间。
int:32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。
long:64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。
float:32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。
double:64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。
boolean:只有true和false两个取值。
char:16位,存储Unicode码,用单引号赋值。
在类中的位置不同
成员变量:在类中方法外面
局部变量:在方法或者代码块中,或者方法的声明上(即在参数列表中)
在内存中的位置不同
成员变量:在堆中(方法区中静态区),成员变量属于对象,对象进堆内存
局部变量:在栈中,局部变量属于方法,方法进栈内存(Java虚拟机栈)
生命周期不同
成员变量:随着对象的创建而存在,随着对象的消失而消失
局部变量:随着方法的调用或代码块的执行而存在,随着方法的调用完毕或者代码块的执行完毕而消失
初始值
成员变量:有默认初始值
局部变量:没有默认初始值,使用前需赋值
成员变量和局部变量的重名问题,使用变量时遵循的原则是就近原则;
可以使用this关键字区分,this.name指的是类中的成员变量,而不是方法内部的。
**抽象类:**如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。
**抽象方法:**它只有声明,而没有具体的实现的方法。
接口:比abstract class更加抽象,是一种特殊的abstract class。用Interface关键字修饰
- 实现
- 抽象类:子类使用extends关键字继承抽象父类。如果子类不是抽象类,就必须要实现抽象父类中声明的所有方法。
- 接口:子类使用implements关键字实现接口。子类需要实现接口中声明的所有方法。
- 一个类仅可以继承一个抽象类,但是可以实现多个接口
- 构造器
- 抽象类:可以有构造器
- 接口:无构造器
- 与正常Java类的区别
- 抽象类:除了不能实例化外,其它的都和正常Java类一样
- 接口:是和Java正常类完全不一样的类型
- 方法的访问修饰符
- 抽象类:抽象方法可以有public、protected、default修饰符
- 接口:接口方法的修饰符默认为public
- 成员变量及其修饰符
- 抽象类:可以有普通成员变量,也可以有静态成员变量,成员变量的访问类型可以任意
- 接口:接口中定义的变量只能是public static final类型(静态成员变量),并且默认即为public static final类型。(必须要赋初值,只能是常量)
- 两者在应用上的区别
- 抽象类:在代码实现方面发挥作用,可以实现代码的重用
- 接口:更多的是在系统架构设计方面发挥作用,主要用于定义模块之间的通信契约。
**相同点:**都不能被实例化,位于继承树的顶端,都包含抽象方法
对于接口:另外Java8允许接口中有 默认方法 、静态方法 。Java9允许接口中有私有方法 。它们都可以有具体的实现。
JDK9 私有方法
https://blog.csdn.net/h294590501/article/details/80303722
【下面这张图错误很多!!!】
静态数据成员是该类所有,该类实例的对象所共有的,类只是数据的一个说明,不占内存,所以静态数据成员不在类中定义,是在类外定义的。也就是在程序一执行就被存放到了数据区了,所以它比对象先生成。
静态成员变量:加static修饰
非静态成员变量:不加 static修饰
静态方法中只能直接访问类中的静态成员(变量、方法),不能访问类中的非静态成员。非静态成员必须要创建实例之后才能访问
静态方法不能引用this和super关键字,因为静态方法不需要创建实例,在引用this或super时可能引用对象还没创建。
静态方法中不能直接调用非静态方法,需要通过对象来访问非静态方法。
子类只能继承、重载、隐藏父类的静态方法,不能重写,也不能把非静态方法写成静态方法
静态成员可以使用类名直接访问,也可以使用对象名进行访问。当然,鉴于他作用的特殊性更推荐用类名访问~~
调用方式不同:成员方法 对象名点方法名(同一个类中可以省略对象),静态方法 类名点方法名 (同一个类中可以省略类名)
加载时期不同:静态方法是随着类的加载就会加载静态变量和静态方法,成员方法是随着创建对象调用方法时加载
静态方法中只能直接访问类中的静态成员(变量、方法),不能访问类中的非静态成员。非静态成员必须要创建实例之后才能访问
非静态方法 可以引用静态方法和静态变量
静态方法不能引用this和super关键字,因为静态方法不需要创建实例,在引用this或super时可能引用对象还没创建
装箱就是自动将基本数据类型转换为包装器类型
拆箱就是 自动将包装器类型转换为基本数据类型。
在Java语言中,new一个对象存储在堆里,我们通过栈中的引用来使用这些对象;但是对于经常用到的一系列类型如int,如果我们用new将其存储在堆里就不是很有效——特别是简单的小的变量。所以就出现了基本类型,对于这些类型不是用new关键字来创建,而是直接将变量的值存储在栈中,因此更加高效。
Java是一个面相对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型)
另外,当需要往ArrayList,HashMap中放东西时,像int,double这种基本类型是放不进去的,因为容器都是装object的,这是就需要这些基本类型的包装器类了。
声明方式不同:
基本类型不使用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
存储方式及位置不同:
基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
初始值不同:
基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null;
使用方式不同:
基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。
public static void main(String[] args) {
int intValue = 100;
Integer boxValue = Integer.valueOf(intValue);
int value = boxValue.intValue();
}必须知道Integer的一个核心数据结构,其内部有一个int类型的成员变量。这也是包装类的设计需求。
基本类型对应包装类的基本设计,很简单,就是包装类中维护了一个对应的基本类型,然后提供构造方法,将外部基本类型的值赋值给其内部维护的这个对应的成员变量。
下面来看intValue()方法,即int value = boxValue.intValue()这一步的语法糖。
public class Integer{
private final int value;
public Integer(int value) { this.value = value; }
public intValue() { return value; }
}
String | StringBuffer | StringBuilder |
---|---|---|
不可变,每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,效率低下,且占用大量有限的内存空间 | 可变类。任何对它指向的字符串的操作都不会产生新的对象。 | 可变类,速度更快。前身是StringBuffer |
线程安全,效率低 | 线程不安全,效率高 | |
多线程操作字符串 | 单线程操作字符 |
StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于: StringBuilder 的方法不是线程安全的(不能同步访问)。
数组是一种引用数据类型,数组引用变量只是一个引用,数组元素和数组变量在内存里面是分开存放的。在内存中分配 了两个空间,一个用来存放数据的引用变量(栈),一个用来存放数组本身(堆)。
此处建议直接看《深入理解Java虚拟机》
**栈内存:**在一个方法执行时,每个方法都会创建自己的内存栈,在这个方法里面定义的变量将会逐个放入这块栈内存中;随方法的执行结束,这个方法的内存栈也将销毁。
**堆内存:**在程序中创建一个对象时,这个对象将会被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常会比较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁。
**栈:**为编译器自动分配和释放,如函数参数、局部变量、临时变量等等
堆:为成员分配和释放,由程序员自己申请、自己释放。否则发生内存泄露。典型为使用new申请的堆内容。
静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。
对象:就是类的一个实例化,new一个实例出来
引用:取一个名字来指代实例化的那个对象。------是一个变量(引用变量)
Demo demo;//创建对象引用
demo = new Demo();//创建对象,/*将对象引用指向对象*/ demo中存储的是地址
明显,一个人可以拥有很多名字,小名绰号艺名笔名,我们可以用周树人去指代这个对象,也可以用鲁迅去指代这个对象,甚至可以用迅哥儿去指代,也就是说一个对象可以拥有很多个对象引用
1、首先是含义不同
1)方法重载是在同一个类中,声明多个同名方法,通过参数列表来区分不同的方法,与参数列表的数量、类型和顺序有关,与修饰符和返回值类型以及抛出异常类型无关
2)方法重写(方法覆盖)的前提是发生在具有继承关系的两个类之间,方法重写有以下规则:
2、方法的重载和重写的作用不同
**重载:**在一个类中为一种行为提供多种实现方式并提高可读性
**重写:**父类方法无法满足子类的要求,子类通过方法重写满足需求
1、==
直接比较的两个对象的堆内存地址,如果相等,则说明这两个引用实际是指向同一个对象地址的。
结果是 true,true,false,那既然==是比较的地址,那么int数据的地址是怎样的呢,String又是怎样的呢?
对于基本数据类型(byte,short,char,int,float,double,long,boolean)来说,他们是作为常量在方法区中的常量池里面以HashSet策略存储起来的,对于这样的字符串 “123” 也是相同的道理,在常量池中,一个常量只会对应一个地址,因此不管是再多的 123,“123” 这样的数据都只会存储一个地址,所以所有他们的引用都是指向的同一块地址,因此基本数据类型和String常量是可以直接通过==来直接比较的。
另外,对于基本数据的包装类型(Byte, Short, Character,Integer,Float, Double,Long, Boolean)除了Float和Double之外,其他的六种都是实现了常量池的,因此对于这些数据类型而言,一般我们也可以直接通过==来判断是否相等。
结果是 true,false。其实是因为 Integer 在常量池中的存储范围为[-128,127],127在这范围内,因此是直接存储于常量池的,而128不在这范围内,所以会在堆内存中创建一个新的对象来保存这个值,所以m,n分别指向了两个不同的对象地址,故而导致了不相等。
2、equals
public boolean equals(Object obj) {
return (this == obj);//因为==实际比较的是地址,所以equals比较的是地址
}
对于equals方法,注意:equals方法不能作用于基本数据类型的变量,equals比较的是是否是同一个对象
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
两个派生接口
Collection接口是List、Set、Queue接口的父接口,该接口里面的方法可用于操作List、Set、Queue集合
判断两个元素会否相等: equals()方法
不允许出现重复元素;
集合中的元素位置无顺序;
有且只有一个值为null的元素。
HashSet : 为快速查找(根据hashCode计算该元素的存储位置)设计的Set。存入HashSet的对象必须定义hashCode()。
TreeSet : 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
Set集合与Collection基本相同,没有提供任何额外的方法。
由于list是一个有序集合,所以List集合里面增加了一些根据索引来操作元素的方法
ArrayList | Vector |
---|---|
有一些方法名很长的方法 | |
线程不安全。如果有超过一个线程修改了ArrayList集合,则程序必须保证该集合的同步性 | 线程安全 |
性能较ArrayList低 |
实现:List list = new ArrayList<>();
由于数组以一块连续的内存区来保存所有的数组元素,所以数组在随机访问时性能最好
- 内部以数组作为底层实现的集合在随机访问时性能都比较好
- 内部以来链表作为底层实现的集合在插入、删除操作时有较好的性能
栈是Vector的一个子类,它实现了一个标准的后进先出的栈。
堆栈只定义了默认构造函数,用来创建一个空栈。 堆栈除了包括由Vector定义的所有方法,也定义了自己的一些方法。
除了由Vector定义的所有方法,自己也定义了一些方法
序号 方法描述
boolean empty() 测试堆栈是否为空
Object peek( ) 测试堆栈是否为空
Object pop( ) 移除堆栈顶部的对象,并作为此函数的值返回该对象
Object push(Object element) 把项压入堆栈顶部
int search(Object element) 返回对象在堆栈中的位置,以 1 为基数
实现:Stack st = new Stack();
特点:线程安全,但性能较差,尽量少用栈
add() 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
remove() 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element () 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer () 添加一个元素并返回true 如果队列已满,则返回false
poll() 移除并返问队列头部的元素 如果队列为空,则返回null
peek() 返回队列头部的元素 如果队列为空,则返回null (要使用前端而不移出该元素)
put() 添加一个元素 如果队列满,则阻塞
take() 移除并返回队列头部的元素 如果队列为空,则阻塞
初始化: Queue queue = new LinkedList<>();
抛出异常 | 返回特殊值 | |
---|---|---|
插入 | add(e) | offer(e) |
删除 | remove() | poll() |
检查 | element() | peek() |
Deque是一个双端队列接口,继承自Queue接口,Deque的实现类是LinkedList、ArrayDeque、LinkedBlockingDeque,其中LinkedList是最常用的。
将键映射到值的对象
一个映射不能包含重复的键(key不可重复)
每个键最多只能映射到一个值(1个key仅能映射一个value)
Map接口和Collection接口的不同
Map是双列的,Collection是单列的
Map的键唯一,Collection的子体系Set是唯一的
Map集合的数据结构针对键有效,跟值无关;Collection集合的数据结构是针对元素有效
方法 | 说明 |
---|---|
void clear() | 删除该Map对象中的所有key-value |
boolean containsKey(Object key) | 查询Map中是否包含指定的key |
boolean containsValue(Object value) | 查询Map是否包含一个或者多个Value |
Object get(Object key) | 根据key获取对应的value;如果Map不包含该key,则返回null |
Boolean isEmpty() | 查询Map是否为空 |
Set keySet() | 返回该Map中所有key组成的Set集合 |
Object put(Object key, Object value) | 添加一个key-value对,如果已存在一个额相同的key,则覆盖 |
Object remove(Object key) | 删除指定key所对应的key-value,返回被删除key所关联的value |
int size() | 返回键值对个数 |
Collection value() | 返回map中所有value组成的Collection |
Set keySet() | 获取集合中所有键的集合 |
Collection values() | 获取集合中所有值的集合 |
看教程吧,这里内容比较多!!!
https://blog.csdn.net/zx1293406/article/details/103926429 (这个感觉更好)
https://blog.csdn.net/lianhuazy167/article/details/66967698
HashMap继承自抽象类AbstractMap,抽象类AbstractMap实现了Map接口。
**产生时间:**Hashtable是java一开始发布时就提供的键值映射的数据结构,而HashMap产生于JDK1.2。虽然Hashtable比HashMap出现的早一些,但是现在Hashtable基本上已经被弃用了。而HashMap已经成为应用最为广泛的一种数据类型了。造成这样的原因一方面是因为Hashtable是线程安全的,效率比较低。另一方面可能是因为Hashtable没有遵循驼峰命名法吧。。。
继承的父类不同:
对外提供的接口不同:Hashtable比HashMap多提供了elments() 和contains() 两个方法。
对Null key 和Null value的支持不同:
线程安全性不同:
Hashtable是线程安全的,它的每个方法中都加入了Synchronized方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步
HashMap不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。使用HashMap时就必须要自己增加同步处理,
虽然HashMap不是线程安全的,但是它的效率会比Hashtable要好很多。在我们的日常使用当中,大部分时间是单线程操作的。HashMap把这部分操作解放出来了。
初始容量大小和每次扩充容量大小的不同:
计算hash值的方法不同:
Hashtable直接使用对象的hashCode。然后再使用除留余数法来获得最终的位置。(hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。)(除法运算是比较耗时的)
HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。HashMap的效率虽然提高了,但是hash冲突却也增加了。因为它得出的hash值的低位相同的概率比较高,而计算位运算为了解决这个问题,HashMap重新根据hashcode计算hash值后,又对hash值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了hash冲突。当然了,为了高效,HashMap只做了一些简单的位处理。从而不至于把使用2 的幂次方带来的效率提升给抵消掉。
https://www.cnblogs.com/heyonggang/p/9112731.html
ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。
对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{...} public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{ ... private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } }
新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长50%。这就意味着,如果你有一个包含大量元素的ArrayList对象,那么最终将有很大的空间会被浪费掉,这个浪费是由ArrayList的工作方式本身造成的
LinkedList中有一个私有的内部类Node,每个Node对象reference列表中的一个元素,同时还有在LinkedList中它的上一个元素和下一个元素(双向链表)。一个有1000个元素的LinkedList对象将有1000个链接在一起的Node对象,每个对象都对应于列表中的一个元素。这样的话,在一个LinkedList结构中将有一个很大的空间开销,因为它要存储这1000个Node对象的相关信息。
总结完数组与链表的优缺点后,是否有一种方法把2种数据结构结合使用,HashMap就很好实现这种结合,先计算出hash数组,在hash冲突后使用链表。
对象在内存中的状态:
强制系统垃圾回收;
finalize()方法:默认机制–清理该对象的资源
https://blog.csdn.net/keep12moving/article/details/102338809
根据JVM规范,JVM把内存划分成了如下几个区域:
堆区(Heap)(最重要)
所有线程共享
Java性能的优化,主要就是针对这部分内存的。所有的对象实例及数组都是在堆上面分配的
在32位系统上最大为2G,64位系统上无限制。可通过-Xms和-Xmx控制,-Xms为JVM启动时申请的最小Heap内存,-Xmx为JVM可申请的最大Heap内存。
虚拟机栈(VM Stack)
JVM虚拟机栈就是我们常说的堆栈的栈(我们常常把内存粗略分为堆和栈),和程序计数器一样,也是线程私有的,生命周期和线程一样,每个方法被执行的时候会产生一个栈帧,用于存储局部变量表、动态链接、操作数、方法出口等信息。方法的执行过程就是栈帧在JVM中出栈和入栈的过程。局部变量表中存放的是各种基本数据类型,如boolean、byte、char、等8种,及引用类型(存放的是指向各个对象的内存地址),因此,它有一个特点:内存空间可以在编译期间就确定,运行期不在改变。这个内存区域会有两种可能的Java异常:StackOverFlowError和OutOfMemoryError。
本地方法栈(Native Method Stack)
用来处理Java中的本地方法的,
程序计数器(Program Counter Register)
有3个是不需要进行垃圾回收的:本地方法栈、程序计数器、虚拟机栈。因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。
https://blog.csdn.net/anjoyandroid/article/details/78609971?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522161828893216780269883017%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=161828893216780269883017&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-2-78609971.first_rank_v2_pc_rank_v29&utm_term=GC%E6%9C%BA%E5%88%B6
如果某个对象已经不存在任何引用,那么它可以被回收。
方法区和堆区需要进行垃圾回收
GC查找算法
引用计数法
可达性分析算法(根搜索算法)。
**以“GC Roots"对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。**可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象。本地方法栈JNI引用的对象。
————————————————
强引用:new出来的对象都是强引用,GC无论如何都不会回收,即使抛出OOM异常。
软引用:只有当JVM内存不足时才会被回收。
弱引用:只要GC,就会立马回收,不管内存是否充足。
虚引用:可以忽略不计,JVM完全不会在乎虚引用,你可以理解为它是来凑数的,凑够”四大天王”。它唯一的作用就是做一些跟踪记录,辅助finalize函数的使用。
补充引用的概念:JDK 1.2之后,对引用进行了扩充,引入了强、软、若、虚四种引用,被标记为这四种引用的对象,在GC时分别有不同的意义:
**强引用(**Strong Reference):就是为刚被new出来的对象所加的引用,它的特点就是,永远不会被回收。
**软引用(**Soft Reference):声明为软引用的类,是可被回收的对象,如果JVM内存并不紧张,这类对象可以不被回收,如果内存紧张,则会被回收。此处有一个问题,既然被引用为软引用的对象可以回收,为什么不去回收呢?其实我们知道,Java中是存在缓存机制的,就拿字面量缓存来说,有些时候,缓存的对象就是当前可有可无的,只是留在内存中如果还有需要,则不需要重新分配内存即可使用,因此,这些对象即可被引用为软引用,方便使用,提高程序性能。
弱引用(Weak Reference):弱引用的对象就是一定需要进行垃圾回收的,不管内存是否紧张,当进行GC时,标记为弱引用的对象一定会被清理回收。
**虚引用(**Phantom Reference):虚引用弱的可以忽略不计,JVM完全不会在乎虚引用,其唯一作用就是做一些跟踪记录,辅助finalize函数的使用。
内存分区:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)
GC算法
标记-清除
最基础的GC算法,将需要进行回收的对象做标记,之后扫描,有标记的进行回收,这样就产生两个步骤:标记和清除。这个算法效率不高,而且在清理完成后会产生内存碎片,这样,如果有大对象需要连续的内存空间时,还需要进行碎片整理,所以,此算法需要改进。
复制算法(新生代回收算法)
标记-整理
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代和新生代,在堆区之外还有一个代就是永久代。
一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
垃圾收集器
Java虚拟机中进行垃圾回收的场所有两个,一个是堆,一个是方法区
什么样的类需要被回收:
(1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
(2)加载该类的ClassLoader已经被回收
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
相同:ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
不同:
Synchronized是通过锁机制,采用了“以时间换空间”的方式,仅提供一份变量,让不同的线程排队访问
ThreadLocal采用了“以空间换时间”的方式,每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。是线程安全的。
解决多线程中数据因并发产生不一致问题
对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。(线程隔离)
线程死去的时候,线程共享变量ThreadLocalMap则销毁。
其中虚线表示弱引用,从该图可以看出,一个Thread维持着一个ThreadLocalMap对象,而该Map对象的key又由提供该value的ThreadLocal对象弱引用提供,所以这就有这种情况:
如果ThreadLocal不设为static的,由于Thread的生命周期不可预知,这就导致了当系统gc时将会回收它,而ThreadLocal对象被回收了,此时它对应key必定为null,这就导致了该key对应的value拿不出来了,而value之前被Thread所引用,所以就存在key为null、value存在强引用导致这个Entry回收不了,从而导致内存泄露。
所以避免内存泄露的方法,是对于ThreadLocal要设为static静态的,除了这个,还必须在线程不使用它的值是手动remove掉该ThreadLocal的值,这样Entry就能够在系统gc的时候正常回收,而关于ThreadLocalMap的回收,会在当前Thread销毁之后进行回收。
注意,空键(即entry.get() == null)意味着不再引用该键,因此可以从表中删除该条目。
共性:
volatile与synchronized都用于保证多线程中数据的安全
区别:
(1)volatile修饰的变量,jvm每次都从主存(主内存)中读取,而不会从寄存器(工作内存)中读取。
而synchronized则是锁住当前变量,同一时刻只有一个线程能够访问当前变量
(2)volatile仅能用在变量级别,而synchronized可用在变量和方法中
(3)volatie仅能实现变量的修改可见性,无法保证变量操作的原子性。而synchronized可以实现变量的修改可见性与原子性
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
内存泄漏( Memory Leak )是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
因为内存泄漏是在堆内存中,所以对我们来说并不是可见的。通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。
java中的对象从使用上分为2种类型,被引用(referenced)的和不被引用(unreferenced)的。垃圾回收只会回收不被引用的对象。被引用的对象,即使已经不再使用了,也不会被回收。因此如果程序中有大量的被引用的无用对象时,就是出现内存泄漏。
当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。
内存泄漏是造成应用程序OOM的主要原因之一。我们知道Android系统为每个应用程序分配的内存是有限的,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过系统分配的内存限额,这就造成了内存溢出从而导致应用Crash。
单例造成的内存泄漏
由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。
非静态内部类创建静态实例造成的内存泄漏
在Activity内部创建了一个非静态内部类的单例(静态实例),每次启动Activity时都会使用该单例的数据。虽然这样避免了资源的重复创建,但是这种写法却会造成内存泄漏。因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。
解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。
Handler造成的内存泄漏
从Android的角度 当Android应用程序启动时,该应用程序的主线程会自动创建一个Looper对象和与之关联的MessageQueue。当主线程中实例化一个Handler对象后,它就会自动与主线程Looper的MessageQueue关联起来。所有发送到MessageQueue的Messag都会持有Handler的引用,所以Looper会据此回调Handle的handleMessage()方法来处理消息。只要MessageQueue中有未处理的Message,Looper就会不断的从中取出并交给Handler处理。另外,主线程的Looper对象会伴随该应用程序的整个生命周期。
Java角度 在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。
对上述的示例进行分析,当MainActivity结束时,未处理的消息持有handler的引用,而handler又持有它所属的外部类也就是MainActivity的引用。这条引用关系会一直保持直到消息得到处理,这样阻止了MainActivity被垃圾回收器回收,从而造成了内存泄漏。
解决方法:将Handler类独立出来或者使用静态内部类,这样便可以避免内存泄漏。
线程造成的内存泄漏
AsyncTask和Runnable都使用了匿名内部类,那么它们将持有其所在Activity的隐式引用。如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。
解决方法:将AsyncTask和Runnable类独立出来或者使用静态内部类,这样便可以避免内存泄漏。
new Thread(new MyRunnable()).start();
new MyAsyncTask(this).execute();
资源未关闭造成的内存泄漏
对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。
初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象缓存起来。当向上滚动ListView时,原先位于最上面的Item的View对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的Item的View对象(初始化时缓存中没有View对象则convertView是null)。
构造Adapter时,没有使用缓存的convertView。
解决方法:在构造Adapter时,使用缓存的convertView。
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
解决方法:在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。
解决方法:为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
1、平常养成良好的代码书写习惯,该销毁的对象要销毁比如destory啊 广播啊 ,涉及到要用到content上下文的优先考虑全局上线文对象。在涉及使用Context时,对于生命周期比Activity长的对象应该使用Application的Context。凡是使用Context优先考虑Application的Context,
2、对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏。
3、对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null。
4、保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。
5、对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
1)将内部类改为静态内部类 2)静态内部类中使用弱引用来引用外部类的成员变量
垃圾回收
https://blog.csdn.net/wanghao112956/article/details/97110973?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control
https://www.runoob.com/java/java8-new-features.html
Lamabda表达式
接口中的默认方法和静态方法
方法引用
orEach()也是jdk8的新特性
比如:list.forEach((s) -> System.out.println(s));—list.forEach(Syetem.out::println);
函数式接口:有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。JDK 1.8 新增加的函数接口:
· java.util.function
Stream:
Optional类
新时间日期API:API LocalDate | LocalTime | LocalDateTime
如何理解面向对象和面向过程?面向过程的优缺点
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,因为性能对他们来说是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展。
面向对象
优点:易维护、易复用、易扩,由于面向对象有封装、继承和多态的特性,故可以设计出低耦合的系统,使系统更加灵活更加易于维护。
缺点:性能比面向过程低。
面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。面向对象是一种以事物为中心的编程思想,注重的是对象本身
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。面向过程就是以过程为中心的编程思想,注重的是实现程序这个过程
https://github.com/yukunsun/concurrent/blob/master/src/test/java/i2021/SynchronizedDemo.java
//锁住LockDemo.class:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象; public void f1() { System.out.println(Thread.currentThread().getName() + "before exec f1"); synchronized (LockDemo.class) { System.out.println(Thread.currentThread().getName() + " exec f1"); sleepLong(); } System.out.println(Thread.currentThread().getName() + "after exec f1"); } //锁住当前对象:其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象 public void f2() { System.out.println(Thread.currentThread().getName() + "before exec f2"); synchronized (this) { System.out.println(Thread.currentThread().getName() + " exec f2"); sleepLong(); } System.out.println(Thread.currentThread().getName() + "after exec f2"); } //锁住一个普通方法:其作用的范围是整个方法,作用的对象是调用这个方法的对象; public synchronized void f3() { System.out.println(Thread.currentThread().getName() + "before exec f3"); System.out.println(Thread.currentThread().getName() + " exec f3"); sleepLong(); System.out.println(Thread.currentThread().getName() + "after exec f3"); } //锁住一个静态方法:其作用的范围是整个方法,作用的对象是这个类的所有对象; public synchronized static void f4() { System.out.println(Thread.currentThread().getName() + "before exec f4"); System.out.println(Thread.currentThread().getName() + " exec f4"); sleepLong(); System.out.println(Thread.currentThread().getName() + "after exec f4"); } }
Java文件被编译成class文件,class文件中处理有类的版本、字段、方法、接口等信息外,还有一个常量池表。(运行时常量池)常量池表用来存放编译期生成的各种字面量及符号引用。
常量池:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEDtnUaa-1628430815575)(C:\Users\wei\AppData\Roaming\Typora\typora-user-images\image-20210429155422219.png)]
封装就是把同一类事物的共性(包括属性和方法)归到同一类中,方便使用。
封装指的是属性私有化,根据需要提供setter和getter方法来访问属性。即隐藏具体属性和实现细节,仅对外开放接口,控制程序中属性的访问级别。
封装也称信息隐藏,是指利用抽象数据类型把数据和基于数据的操作封装起来,使其成为一个不可分割的整体,数据隐藏在抽象数据内部,尽可能的隐藏数据细节,只保留一些接口使其与外界发生联系。也就是说用户无需知道内部的数据和方法的具体实现细节,只需根据留在外部的接口进行操作就行。
为了实现良好的封装,我们通常将类的成员变量声明为private,为了能够在外部使用,可以通过定义public方法来对这个变量来访问。对一个变量的操作,一般有读取和赋值2个操作,我们分别定义2个方法来实现这2个操作,一个是getXX(XX表示要访问的成员变量的名字)用来读取这个成员变量,另一个是setXX()用来对这个变量赋值
3. 封装的优点
3.1 将变化隔离 3.2 便于使用 3.3 提高重用性 3.4 提高安全性
- 1
- 2
- 3
- 4
4. 封装的缺点:
将变量等使用private修饰,或者封装进方法内,使其不能直接被访问,增加了访问步骤与难度!
继承是指将多个相同的属性和方法提取出来,新建一个父类。
Java中一个类只能继承一个父类,且只能继承访问权限非private的属性和方法。 子类可以重写父类中的方法,命名与父类中同名的属性。子类可以直接访问父类的非私有化成员变量,访问父类的私有化成员变量可以使用super.get()方法。
继承目的:代码复用。
多态可以分为两种:设计时多态和运行时多态。
我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。这也是为什么有时候多态方法又被称为延迟方法的原因。
直接看HashMap
HashSet的底层实现是HashMap
Set不能有重复的元素,HashMap不允许有重复的键
Set中有且只有1个元素的值为null
HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
在HashSet中:
private static final Object PRESENT = new Object();
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L; private transient HashMap<E,Object> map; // 底层使用HashMap来保存HashSet中所有元素。 private static final Object PRESENT = new Object();// 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。 //实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。 public HashSet() { map = new HashMap<>(); } public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); } public Iterator<E> iterator() { return map.keySet().iterator(); } public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean contains(Object o) { return map.containsKey(o); } public boolean add(E e) { return map.put(e, PRESENT)==null; } public boolean remove(Object o) { return map.remove(o)==PRESENT; } public void clear() { map.clear(); } ...... }
插入:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
当有新值加入时,底层的HashMap会判断Key值是否存在(HashMap细节请移步深入理解HashMap)
删除:
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
同HashMap删除原理:底层实际调用HashMap的remove方法删除指定Entry。
【问题1】既然hashset基于hashmap实现,你说一下 hashset的add方法中,为什么要在map.put的val上放上一个object类型的静态常量present?
首先要看hashmap的put 方法的返回值,map对象在调用put的时候传入一个key和val,会对其key进行一个算法得到一个位置,会把put的数据放到其位置上,如果该位置上已经存在当前key,会对其key映射的val给替换掉,并且返回之前的val,则返回null。
好了,既然put的返回值原理搞清楚了,就要去看看 set 的add方法的实现,add方法是调用了put方法,并且把key放在了put的key上,val放了一个hashset类的静态常量present, 如果put 返回的是null,不是present,就说明 put的key是不存在的,add也会返回true,如果put返回的是present就说明之前的key是存在的,并不是说没有put上,所以add方法返回的false并不是存失败的意思,而是map.put的key是已经存在的,而且已经把val给替换了。
【问题2】既然hashset基于hashmap实现,你说一下 hashset的remove方法中,为什么要在map.remove key 完了之后要和present进行一个等值比较呢?
首先要看hashmap的remove的返回原因,hashmap的remove方法删除一个key的时候会把之前的val的返回出来,这点弄清楚。
就要明白为什么set在remove的时候要和present进行对比,如果map中remove的返回是present,说明key是存在的,返回true,这点要结合set的add进行一个联想,如果返回的不是present,说明这个key在set对象里面的hashmap中是不存在的,所以返回的是false。
注意:
为什么重写equals()方法也要重写hashCode()方法?
使用hashcode方法提前校验,可以避免每一次比对都调用equals方法,提高效率
保证是同一个对象。如果重写了equals方法,而没有重写hashcode方法,会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。(在HashMap中会可能会将equals()返回true的对象存为不同的元素,因为不同的对象会被判断为不同的key,分开存储)
并不是存失败的意思,而是map.put的key是已经存在的,而且已经把val给替换了。
【问题2】既然hashset基于hashmap实现,你说一下 hashset的remove方法中,为什么要在map.remove key 完了之后要和present进行一个等值比较呢?
首先要看hashmap的remove的返回原因,hashmap的remove方法删除一个key的时候会把之前的val的返回出来,这点弄清楚。
就要明白为什么set在remove的时候要和present进行对比,如果map中remove的返回是present,说明key是存在的,返回true,这点要结合set的add进行一个联想,如果返回的不是present,说明这个key在set对象里面的hashmap中是不存在的,所以返回的是false。
注意:
为什么重写equals()方法也要重写hashCode()方法?
使用hashcode方法提前校验,可以避免每一次比对都调用equals方法,提高效率
保证是同一个对象。如果重写了equals方法,而没有重写hashcode方法,会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。(在HashMap中会可能会将equals()返回true的对象存为不同的元素,因为不同的对象会被判断为不同的key,分开存储)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。