当前位置:   article > 正文

面试题解--Java(全)_java的check的返回值是多少

java的check的返回值是多少

Java

线程不安全的线程安全的
StringBuilderStringBuffer
ArrayListVector
HashMapHashtable

基本数据类型

字符型:char

布尔型:Boolean

数值型:

  • 整数类型:byte(1字节), short(2字节), int(4字节), long(8字节)
  • 浮点型:float(4字节), double(8字节)

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码,用单引号赋值。

(1)成员变量和局部变量的区别

  1. 在类中的位置不同
    成员变量:在类中方法外面
    局部变量:在方法或者代码块中,或者方法的声明上(即在参数列表中)

  2. 内存中的位置不同
    成员变量:在中(方法区中静态区),成员变量属于对象,对象进堆内存
    局部变量:在中,局部变量属于方法,方法进栈内存(Java虚拟机栈)

  3. 生命周期不同
    成员变量:随着对象的创建而存在,随着对象的消失而消失
    局部变量:随着方法的调用或代码块的执行而存在,随着方法的调用完毕或者代码块的执行完毕而消失

  4. 初始值
    成员变量:有默认初始值
    局部变量:没有默认初始值,使用前需赋值

成员变量和局部变量的重名问题,使用变量时遵循的原则是就近原则;
可以使用this关键字区分,this.name指的是类中的成员变量,而不是方法内部的。

(2)抽象类和接口的区别 important

**抽象类:**如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。

**抽象方法:**它只有声明,而没有具体的实现的方法。

接口:比abstract class更加抽象,是一种特殊的abstract class。用Interface关键字修饰

  1. 实现
    • 抽象类:子类使用extends关键字继承抽象父类。如果子类不是抽象类,就必须要实现抽象父类中声明的所有方法。
    • 接口:子类使用implements关键字实现接口。子类需要实现接口中声明的所有方法。
    • 一个类仅可以继承一个抽象类,但是可以实现多个接口
  2. 构造器
    • 抽象类:可以有构造器
    • 接口:无构造器
  3. 与正常Java类的区别
    • 抽象类:除了不能实例化外,其它的都和正常Java类一样
    • 接口:是和Java正常类完全不一样的类型
  4. 方法的访问修饰符
    • 抽象类:抽象方法可以有public、protected、default修饰符
    • 接口:接口方法的修饰符默认为public
  5. 成员变量及其修饰符
    • 抽象类:可以有普通成员变量,也可以有静态成员变量,成员变量的访问类型可以任意
    • 接口:接口中定义的变量只能是public static final类型(静态成员变量),并且默认即为public static final类型。(必须要赋初值,只能是常量)
  6. 两者在应用上的区别
    • 抽象类:在代码实现方面发挥作用,可以实现代码的重用
    • 接口:更多的是在系统架构设计方面发挥作用,主要用于定义模块之间的通信契约。

**相同点:**都不能被实例化,位于继承树的顶端,都包含抽象方法

对于接口另外Java8允许接口中有 默认方法 、静态方法 。Java9允许接口中有私有方法 。它们都可以有具体的实现。

image-20210420094501881

JDK9 私有方法

  1. 为什么要出现私有方法
    问题描述:在接口中我们需要抽取一个公共方法,用来解决两个默认方法之间重复代码 的问题,但是这个共有方法不应该让实现类使用,应该是私有化的。因此要解决这个问题就要考虑使用私有方法。
    因此,接口中出现私有方法的目的就是解决多个默认方法之间的重复代码问题。
  2. 普通私有方法是解决多个默认方法之间重复代码的问题;private void method();
  3. 静态私有方法是解决多个静态方法之间的代码重复问题。 private static void method()
  4. 如何使用私有方法
    使用过程的话其实和普通类中声明一个私有方法然后使用时一样的,在接口中创建一个私有方法当然只能在本接口内部使用。这里要说的是由于JDK8中出现了默认的方法和静态方法,因此此时的私有方法就可以使用了。

image-20210420095734552

https://blog.csdn.net/h294590501/article/details/80303722

image-20210420102119755

【下面这张图错误很多!!!】

image-20210411115118941

(3)抽象类实现某个接口能否不实现接口方法

  • 由普通的类来实现接口,必须将接口所有抽象方法重写
  • 由抽象类来实现接口,则不必重写接口的方法。可以全部不重写或只重写一部分方法。

(4)静态成员变量

静态数据成员

静态数据成员是该类所有,该类实例的对象所共有的,类只是数据的一个说明,不占内存,所以静态数据成员不在类中定义,是在类外定义的。也就是在程序一执行就被存放到了数据区了,所以它比对象先生成

  • 静态成员变量:加static修饰

  • 非静态成员变量:不加 static修饰

  • 静态方法中只能直接访问类中的静态成员(变量、方法),不能访问类中的非静态成员。非静态成员必须要创建实例之后才能访问

    • 如果希望在静态方法中调用非静态变量,可以通过创建类的(实例)对象,然后通过对象来访问非静态变量
    • 在普通成员方法中,则可以直接访问同类的非静态变量和静态变量
  • 静态方法不能引用this和super关键字,因为静态方法不需要创建实例,在引用this或super时可能引用对象还没创建

  • 静态方法中不能直接调用非静态方法,需要通过对象来访问非静态方法。

  • 子类只能继承、重载、隐藏父类的静态方法,不能重写,也不能把非静态方法写成静态方法

静态成员可以使用类名直接访问,也可以使用对象名进行访问。当然,鉴于他作用的特殊性更推荐用类名访问~~

(5)静态方法和成员方法的区别

  • 调用方式不同:成员方法 对象名点方法名(同一个类中可以省略对象),静态方法 类名点方法名 (同一个类中可以省略类名)

  • 加载时期不同:静态方法是随着类的加载就会加载静态变量和静态方法,成员方法是随着创建对象调用方法时加载

  • 静态方法中只能直接访问类中的静态成员(变量、方法),不能访问类中的非静态成员。非静态成员必须要创建实例之后才能访问

  • 非静态方法 可以引用静态方法和静态变量

  • 静态方法不能引用this和super关键字,因为静态方法不需要创建实例,在引用this或super时可能引用对象还没创建

(6)装箱与拆箱

装箱就是自动将基本数据类型转换为包装器类型

拆箱就是 自动将包装器类型转换为基本数据类型。

在Java语言中,new一个对象存储在堆里,我们通过栈中的引用来使用这些对象;但是对于经常用到的一系列类型如int,如果我们用new将其存储在堆里就不是很有效——特别是简单的小的变量。所以就出现了基本类型,对于这些类型不是用new关键字来创建,而是直接将变量的值存储在栈中,因此更加高效。

【问题1】有了基本类型为什么还要有包装类型呢?

Java是一个面相对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型)

另外,当需要往ArrayList,HashMap中放东西时,像int,double这种基本类型是放不进去的,因为容器都是装object的,这是就需要这些基本类型的包装器类了。

【问题2】基本类型与包装器类型的区别:

  1. 声明方式不同:
    基本类型不使用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;

  2. 存储方式及位置不同:
    基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后通过引用来使用;

  3. 初始值不同:
    基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null;

  4. 使用方式不同:
    基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。

img

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; }

}

(7)String,StringBuffer,StringBuilder的区别

StringStringBufferStringBuilder
不可变,每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,效率低下,且占用大量有限的内存空间可变类。任何对它指向的字符串的操作都不会产生新的对象。可变类,速度更快。前身是StringBuffer
线程安全,效率低线程不安全,效率高
多线程操作字符串单线程操作字符

StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于: StringBuilder 的方法不是线程安全的(不能同步访问)。


(8)数组

数组是一种引用数据类型,数组引用变量只是一个引用,数组元素和数组变量在内存里面是分开存放的。在内存中分配 了两个空间,一个用来存放数据的引用变量(栈),一个用来存放数组本身(堆)。

  • 实际数组对象被存储在堆(heap)内存中
  • 引用该数组对象的数组引用变量如果是局部变量,name它被存储在栈(stack)中

(9)栈内存和堆内存

此处建议直接看《深入理解Java虚拟机》

**栈内存:**在一个方法执行时,每个方法都会创建自己的内存栈,在这个方法里面定义的变量将会逐个放入这块栈内存中;随方法的执行结束,这个方法的内存栈也将销毁。

**堆内存:**在程序中创建一个对象时,这个对象将会被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常会比较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁。

**栈:**为编译器自动分配和释放,如函数参数、局部变量、临时变量等等
堆:为成员分配和释放,由程序员自己申请、自己释放。否则发生内存泄露。典型为使用new申请的堆内容。

静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据常量

(10)对象 和 引用

对象:就是类的一个实例化,new一个实例出来

引用:取一个名字来指代实例化的那个对象。------是一个变量(引用变量)

Demo demo;//创建对象引用
demo = new Demo();//创建对象,/*将对象引用指向对象*/ demo中存储的是地址
  • 1
  • 2

明显,一个人可以拥有很多名字,小名绰号艺名笔名,我们可以用周树人去指代这个对象,也可以用鲁迅去指代这个对象,甚至可以用迅哥儿去指代,也就是说一个对象可以拥有很多个对象引用

img

image-20210411145828824

(11)方法重载和方法覆盖(重写)的区别

1、首先是含义不同

1)方法重载是在同一个类中,声明多个同名方法,通过参数列表来区分不同的方法,与参数列表的数量、类型和顺序有关,与修饰符和返回值类型以及抛出异常类型无关

2)方法重写(方法覆盖)的前提是发生在具有继承关系的两个类之间,方法重写有以下规则:

  • 参数列表,返回值,方法名必须保持一致
  • 重写方法的访问权限范围必须大于等于父类方法
  • 重写方法的抛出异常类型范围不能大于父类方法

2、方法的重载和重写的作用不同

**重载:**在一个类中为一种行为提供多种实现方式并提高可读性

**重写:**父类方法无法满足子类的要求,子类通过方法重写满足需求
在这里插入图片描述

在这里插入图片描述

(12)==和equals

1、==

直接比较的两个对象的堆内存地址,如果相等,则说明这两个引用实际是指向同一个对象地址的。

img

结果是 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之外,其他的六种都是实现了常量池的,因此对于这些数据类型而言,一般我们也可以直接通过==来判断是否相等。

img

结果是 true,false。其实是因为 Integer 在常量池中的存储范围为[-128,127],127在这范围内,因此是直接存储于常量池的,而128不在这范围内,所以会在堆内存中创建一个新的对象来保存这个值,所以m,n分别指向了两个不同的对象地址,故而导致了不相等。

2、equals

public boolean equals(Object obj) {
    return (this == obj);//因为==实际比较的是地址,所以equals比较的是地址
}
  • 1
  • 2
  • 3

对于equals方法,注意:equals方法不能作用于基本数据类型的变量,equals比较的是是否是同一个对象

  • 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址

  • 诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。

(13) Java集合类

img

两个派生接口

  • Collection
    • Set:无序,不可重复的集合
      • HashSet
      • TreeSet
    • List:有序,可重复的集合。根据元素索引访问
      • ArrayList
    • Queue:先进先出(双端队列首位i都可进出)
      • LinkedList (继承自Deque)
      • ArrayDeque(继承自Deque)
  • Map:有映射关系的集合。键值对 key—value
    • key不可重复:标识每一项数据
    • 通过key来访问value

1、Collection接口

Collection接口是List、Set、Queue接口的父接口,该接口里面的方法可用于操作List、Set、Queue集合

  • boolean add(Object o):添加元素
  • boolean addAll(Collection c)
  • boolean contains(Object o):判断集合里面是否包含指定元素
  • boolean containsAll(Collection c)
  • boolean isEmpty():判断集合是否为空
  • boolean remove(Object o):删除指定元素
  • boolean removeAll(Collection c)
  • Object[] toArray(); 将集合转换成数组
  • int size():返回该集合的元素

判断两个元素会否相等: equals()方法

1.1 Set集合

不允许出现重复元素;
集合中的元素位置无顺序;
有且只有一个值为null的元素。

HashSet :快速查找(根据hashCode计算该元素的存储位置)设计的Set。存入HashSet的对象必须定义hashCode()。

  • 当把一个对象放入HashSet中时,如果需要重写该对象的equals()方法,则也应该重写其hashCode()方法。保证这两个方法的返回值是一致的。
  • Hash算法可以根据hashCode的值计算出该元素的存储位置

TreeSet : 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。

Set集合与Collection基本相同,没有提供任何额外的方法。

1.2 List集合

由于list是一个有序集合,所以List集合里面增加了一些根据索引来操作元素的方法

  • void add(int index, Object element)
  • void add(Object o)
  • Object get(int index)
  • int lastIndexOf(Object o)
  • int indexOf(Object o)
  • Object remove(int index)
  • Object set(int index, Object element):将索引出的元素替换成element对象,返回被替换旧元素

image-20210411222501299

ArrayListVector
有一些方法名很长的方法
线程不安全。如果有超过一个线程修改了ArrayList集合,则程序必须保证该集合的同步性线程安全
性能较ArrayList低

实现:List list = new ArrayList<>();

由于数组以一块连续的内存区来保存所有的数组元素,所以数组在随机访问时性能最好

  • 内部以数组作为底层实现的集合在随机访问时性能都比较好
  • 内部以来链表作为底层实现的集合在插入、删除操作时有较好的性能

image-20210411224535198

image-20210411224559049

Stack
  • Stack是Vector的子类
  • 先进后出 LIFO

栈是Vector的一个子类,它实现了一个标准的后进先出的栈。
堆栈只定义了默认构造函数,用来创建一个空栈。 堆栈除了包括由Vector定义的所有方法,也定义了自己的一些方法。
除了由Vector定义的所有方法,自己也定义了一些方法

序号 方法描述

  • boolean empty() 测试堆栈是否为空

  • Object peek( ) 测试堆栈是否为空

  • Object pop( ) 移除堆栈顶部的对象,并作为此函数的值返回该对象

  • Object push(Object element) 把项压入堆栈顶部

  • int search(Object element) 返回对象在堆栈中的位置,以 1 为基数

实现:Stack st = new Stack();

特点:线程安全,但性能较差,尽量少用栈

1.3 Queue 集合
(一)jdk1.5中的阻塞队列(Queue)的操作:
  • 只允许在表的前端(front)进行删除操作
  • 而在表的后端(rear)进行插入操作
  • 先进先出FIFO—first in first out

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

Deque是一个双端队列接口,继承自Queue接口,Deque的实现类是LinkedList、ArrayDeque、LinkedBlockingDeque,其中LinkedList是最常用的。

image-20210406154520614

2、Map集合

键映射到值的对象
一个映射不能包含重复的键(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()获取集合中所有值的集合
HashMap 和 HashCode

看教程吧,这里内容比较多!!!

https://blog.csdn.net/zx1293406/article/details/103926429 (这个感觉更好)

https://blog.csdn.net/lianhuazy167/article/details/66967698

HashMap继承自抽象类AbstractMap,抽象类AbstractMap实现了Map接口

img


【问题1】HashMap和HashTable的区别

  1. **产生时间:**Hashtable是java一开始发布时就提供的键值映射的数据结构,而HashMap产生于JDK1.2。虽然Hashtable比HashMap出现的早一些,但是现在Hashtable基本上已经被弃用了。而HashMap已经成为应用最为广泛的一种数据类型了。造成这样的原因一方面是因为Hashtable是线程安全的,效率比较低。另一方面可能是因为Hashtable没有遵循驼峰命名法吧。。。

  2. 继承的父类不同

    • HashMap是继承自AbstractMap类,
    • HashTable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。(Dictionary类是一个已经被废弃的类(见其源码中的注释)。父类都被废弃,自然而然也没人用它的子类Hashtable了。)
  3. 对外提供的接口不同:Hashtable比HashMap多提供了elments() 和contains() 两个方法。

  4. 对Null key 和Null value的支持不同

    • Hashtable既不支持Null key也不支持Null value。
    • HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
  5. 线程安全性不同

    • Hashtable是线程安全的,它的每个方法中都加入了Synchronized方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步

    • HashMap不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。使用HashMap时就必须要自己增加同步处理,

    虽然HashMap不是线程安全的,但是它的效率会比Hashtable要好很多。在我们的日常使用当中,大部分时间是单线程操作的。HashMap把这部分操作解放出来了。

    • 当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
  6. 初始容量大小和每次扩充容量大小的不同:

    • Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。
    • HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
  7. 计算hash值的方法不同:

    • Hashtable直接使用对象的hashCode。然后再使用除留余数法来获得最终的位置。(hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。)(除法运算是比较耗时的)

    • HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。HashMap的效率虽然提高了,但是hash冲突却也增加了。因为它得出的hash值的低位相同的概率比较高,而计算位运算为了解决这个问题,HashMap重新根据hashcode计算hash值后,又对hash值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了hash冲突。当然了,为了高效,HashMap只做了一些简单的位处理。从而不至于把使用2 的幂次方带来的效率提升给抵消掉。

【问题2】(HashMap、Hashtable、ConcurrentHashMap的原理与区别)

https://www.cnblogs.com/heyonggang/p/9112731.html

【问题3】ArrayList和LinkedList的区别

  1. ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

    • ArrayList内部是使用可増长数组实现的,所以是用get和set方法是花费常数时间的,但是如果插入元素和删除元素,除非插入和删除的位置都在表末尾,否则代码开销会很大,因为里面需要数组的移动。
    • LinkedList是使用双链表实现的,所以get会非常消耗资源,除非位置离头部很近(访问元素效率低)。但是插入和删除元素花费常数时间。
  2. 对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。

  3. 对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。

  4. ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

    • 对ArrayList和LinkedList而言,在列表末尾增加一个元素的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长50%。这就意味着,如果你有一个包含大量元素的ArrayList对象,那么最终将有很大的空间会被浪费掉,这个浪费是由ArrayList的工作方式本身造成的

LinkedList中有一个私有的内部类Node,每个Node对象reference列表中的一个元素,同时还有在LinkedList中它的上一个元素和下一个元素(双向链表)。一个有1000个元素的LinkedList对象将有1000个链接在一起的Node对象,每个对象都对应于列表中的一个元素。这样的话,在一个LinkedList结构中将有一个很大的空间开销,因为它要存储这1000个Node对象的相关信息。

总结完数组与链表的优缺点后,是否有一种方法把2种数据结构结合使用HashMap就很好实现这种结合,先计算出hash数组,在hash冲突后使用链表。


(14)垃圾回收

  1. 垃圾回收机制只回收堆内存中的对象,不会回收任何物理资源
  2. 程序无法精确控制垃圾回收的运行,垃圾回收会在合适的时候进行
  3. 回收任何对象之前会先调用它的finalize()方法,该方法可能会使对象重新复活,从而导致垃圾回收机制取消回收

对象在内存中的状态

  1. 可达状态:有一个以上的引用变量引用该对象
  2. 可恢复状态:如果程序中的某个对象不再有任何引用变量引用它,它就进入可恢复状态。回收对象前会调用对象的finalize()方法进行资源清理。如果系统在调用finalize()时重新让一个引用变量引用该对象,则这个对象再次进入可达状态;负责该对象进入不可达状态
  3. 不可达状态:对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性失去引用,最后变成不可达状态。只要一个对象真正处于不可达状态是,系统才会真正回收该对象所占有的资源

强制系统垃圾回收

  1. 调用System类的gc()静态方法:System.gc();
  2. 调用Runtime对象的gc()实例方法:Runtime.getRuntime(),gc()

finalize()方法:默认机制–清理该对象的资源

image-20210413121958833 image-20210413122553299

(15)JVM的内存划分

https://blog.csdn.net/keep12moving/article/details/102338809

在这里插入图片描述

根据JVM规范,JVM把内存划分成了如下几个区域:

  1. 方法区(Method Area)
  • 方法区是所有线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量等数据,一般来说,方法区属于持久代(关于持久代,会在GC部分详细介绍,除了持久代,还有新生代和旧生代),也难怪Java规范将方法区描述为堆的一个逻辑部分,但是它不是堆
  1. 堆区(Heap)(最重要)

    • 所有线程共享

    • Java性能的优化,主要就是针对这部分内存的。所有的对象实例及数组都是在堆上面分配的

    • 在32位系统上最大为2G,64位系统上无限制。可通过-Xms和-Xmx控制,-Xms为JVM启动时申请的最小Heap内存,-Xmx为JVM可申请的最大Heap内存。

  2. 虚拟机栈(VM Stack)

    JVM虚拟机栈就是我们常说的堆栈的(我们常常把内存粗略分为堆和栈),和程序计数器一样,也是线程私有的,生命周期和线程一样,每个方法被执行的时候会产生一个栈帧,用于存储局部变量表、动态链接、操作数、方法出口等信息。方法的执行过程就是栈帧在JVM中出栈和入栈的过程。局部变量表中存放的是各种基本数据类型,如boolean、byte、char、等8种,及引用类型(存放的是指向各个对象的内存地址),因此,它有一个特点:内存空间可以在编译期间就确定,运行期不在改变。这个内存区域会有两种可能的Java异常:StackOverFlowError和OutOfMemoryError。

  3. 本地方法栈(Native Method Stack)

    用来处理Java中的本地方法的,

  4. 程序计数器(Program Counter Register)

    • 每个线程都必须有一个独立的程序计数器,这类计数器为线程私有的内存
    • 内存区是唯一一个在Java规范中没有任何OutOfMemoryError情况的区域

有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

(16)GC—Garbage Collection

如果某个对象已经不存在任何引用,那么它可以被回收。

方法区和堆区需要进行垃圾回收

  1. GC查找算法

    • 引用计数法

      • 每个对象添加一个引用计数器,每被引用一次,计数器加1,失去引用,计数器减1,当计数器在一段时间内保持为0时,该对象就认为是可以被回收得了。但是,这个算法有明显的缺陷:当两个对象相互引用,但是二者已经没有作用时,按照常规,应该对其进行垃圾回收,但是其相互引用,又不符合垃圾回收的条件,因此无法完美处理这块内存清理,因此Sun的JVM并没有采用引用计数算法来进行垃圾回收。
    • 可达性分析算法(根搜索算法)。

    • **以“GC Roots"对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。**可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象。本地方法栈JNI引用的对象。
      ————————————————

      强引用:new出来的对象都是强引用,GC无论如何都不会回收,即使抛出OOM异常。
      软引用:只有当JVM内存不足时才会被回收。
      弱引用:只要GC,就会立马回收,不管内存是否充足。
      虚引用:可以忽略不计,JVM完全不会在乎虚引用,你可以理解为它是来凑数的,凑够”四大天王”。它唯一的作用就是做一些跟踪记录,辅助finalize函数的使用。

补充引用的概念:JDK 1.2之后,对引用进行了扩充,引入了强、软、若、虚四种引用,被标记为这四种引用的对象,在GC时分别有不同的意义:

  1. **强引用(**Strong Reference):就是为刚被new出来的对象所加的引用,它的特点就是,永远不会被回收。

  2. **软引用(**Soft Reference):声明为软引用的类,是可被回收的对象,如果JVM内存并不紧张,这类对象可以不被回收,如果内存紧张,则会被回收。此处有一个问题,既然被引用为软引用的对象可以回收,为什么不去回收呢?其实我们知道,Java中是存在缓存机制的,就拿字面量缓存来说,有些时候,缓存的对象就是当前可有可无的,只是留在内存中如果还有需要,则不需要重新分配内存即可使用,因此,这些对象即可被引用为软引用,方便使用,提高程序性能。

  3. 弱引用(Weak Reference):弱引用的对象就是一定需要进行垃圾回收的,不管内存是否紧张,当进行GC时,标记为弱引用的对象一定会被清理回收。

  4. **虚引用(**Phantom Reference):虚引用弱的可以忽略不计,JVM完全不会在乎虚引用,其唯一作用就是做一些跟踪记录,辅助finalize函数的使用。

image-20210417145620129
  1. 内存分区:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)

    • 新生代适合那些生命周期较短,频繁创建及销毁的对象。大致分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpace 和ToSpace。新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,
    • 旧生代适合生命周期相对较长的对象。用于存放新生代中经过多次垃圾回收仍然存活的对象,例如缓存对象。
    • 持久代在Sun HotSpot(在Sun的JVM中)中就是指方法区(有些JVM中根本就没有持久代这中说法)。主要存放常量及类的一些信息默认最小值为16MB,最大值为64MB
  2. GC算法

    • 标记-清除

      最基础的GC算法,将需要进行回收的对象做标记,之后扫描,有标记的进行回收,这样就产生两个步骤:标记和清除。这个算法效率不高,而且在清理完成后会产生内存碎片,这样,如果有大对象需要连续的内存空间时,还需要进行碎片整理,所以,此算法需要改进。

    • 复制算法(新生代回收算法)

      • “复制”算法是为了解决“标记-清除”的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等的复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。
      • 复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。
    • 标记-整理

      • 标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
    • 分代收集算法

      分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代和新生代,在堆区之外还有一个代就是永久代。

      一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

      • 新生代—复制算法
      • 老生代—"标记-清理"或者"标记-整理"算法
      • 永久代
      • 特别地,在分代收集算法中,对象的存储具有以下特点:
        1. 对象优先在 Eden 区分配。
        2. 大对象直接进入老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组。
        3. 长期存活的对象将进入老年代,默认为 15 岁。当对象在 Survivor 区躲过一次 GC 的话,其对象年龄便会加 1,默认情况下,如果对象年龄达到 15 岁,就会移动到老年代中。
    • img
  3. 垃圾收集器

    image-20210413132429519

Java虚拟机中进行垃圾回收的场所有两个一个是堆,一个是方法区

img

什么样的类需要被回收:

(1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;

(2)加载该类的ClassLoader已经被回收

(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

【问题1】为什么要分为Eden和Survivor?为什么要设置两个Survivor区?

  • 如果没有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两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

image-20210417160140432


(17)ThreadLocal 与 Synchronized区别

相同:ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

不同

  • Synchronized是通过锁机制,采用了“以时间换空间”的方式,仅提供一份变量,让不同的线程排队访问

    • 解决多线程共享数据同步问题
    • 利用锁的机制,使变量或代码块在某一时该只能被一个线程访问
  • ThreadLocal采用了“以空间换时间”的方式,每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。是线程安全的。

    • 解决多线程中数据因并发产生不一致问题

    • 对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。(线程隔离)

    • 线程死去的时候,线程共享变量ThreadLocalMap则销毁。

image-20210417104101569

image-20210417110613783

这里写图片描述

其中虚线表示弱引用,从该图可以看出,一个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销毁之后进行回收。

image-20210417111147828

注意,空键(即entry.get() == null)意味着不再引用该键,因此可以从表中删除该条目。

image-20210417112156191

【问题1】volatile与synchronized的区别

共性:

volatile与synchronized都用于保证多线程中数据的安全

区别:

(1)volatile修饰的变量,jvm每次都从主存(主内存)中读取,而不会从寄存器(工作内存)中读取。

而synchronized则是锁住当前变量,同一时刻只有一个线程能够访问当前变量

(2)volatile仅能用在变量级别,而synchronized可用在变量和方法中

(3)volatie仅能实现变量的修改可见性,无法保证变量操作的原子性。而synchronized可以实现变量的修改可见性与原子性

(18)什么是内存泄漏MemoryLeak?内存溢出OutOfMemory?

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。

内存泄漏( Memory Leak )是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

memory leak会最终会导致out of memory!

image-20210417113505785

因为内存泄漏是在堆内存中,所以对我们来说并不是可见的。通常我们可以借助MAT、LeakCanary等工具来检测应用程序是否存在内存泄漏。

【问题1】内存泄漏的原因

java中的对象从使用上分为2种类型,被引用(referenced)的和不被引用(unreferenced)的。垃圾回收只会回收不被引用的对象。被引用的对象,即使已经不再使用了,也不会被回收。因此如果程序中有大量的被引用的无用对象时,就是出现内存泄漏。

当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏

【问题2】内存泄漏对程序的影响?

内存泄漏是造成应用程序OOM的主要原因之一。我们知道Android系统为每个应用程序分配的内存是有限的,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过系统分配的内存限额,这就造成了内存溢出从而导致应用Crash。

【问题3】常见的内存泄漏及解决方法

  1. 单例造成的内存泄漏

    由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。

  2. 非静态内部类创建静态实例造成的内存泄漏

    在Activity内部创建了一个非静态内部类单例(静态实例),每次启动Activity时都会使用该单例的数据。虽然这样避免了资源的重复创建,但是这种写法却会造成内存泄漏。因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。

    解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。

  3. 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类独立出来或者使用静态内部类,这样便可以避免内存泄漏。

  4. 线程造成的内存泄漏

    • AsyncTask和Runnable都使用了匿名内部类,那么它们将持有其所在Activity的隐式引用。如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。

    • 解决方法:将AsyncTask和Runnable类独立出来或者使用静态内部类,这样便可以避免内存泄漏。

    • new Thread(new MyRunnable()).start();
      new MyAsyncTask(this).execute();
      
      • 1
      • 2
  5. 资源未关闭造成的内存泄漏

    对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。

    6、使用ListView时造成的内存泄漏

    初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象缓存起来。当向上滚动ListView时,原先位于最上面的Item的View对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的Item的View对象(初始化时缓存中没有View对象则convertView是null)。

    构造Adapter时,没有使用缓存的convertView。
    解决方法:在构造Adapter时,使用缓存的convertView。

    7、集合容器中的内存泄露

    我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
    解决方法:在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

    8、WebView造成的泄露

    当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。
    解决方法:为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

【问题4】如何避免内存泄漏?

1、平常养成良好的代码书写习惯,该销毁的对象要销毁比如destory啊 广播啊 ,涉及到要用到content上下文的优先考虑全局上线文对象。在涉及使用Context时,对于生命周期比Activity长的对象应该使用Application的Context。凡是使用Context优先考虑Application的Context,

2、对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏。

3、对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null。

4、保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。

5、对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:

​ 1)将内部类改为静态内部类 2)静态内部类中使用弱引用来引用外部类的成员变量

image-20210417133214456

(18)

垃圾回收

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

(19)Java8新特性

https://www.runoob.com/java/java8-new-features.html

  1. Lamabda表达式

  2. 接口中的默认方法和静态方法

  3. 方法引用

    orEach()也是jdk8的新特性

    比如:list.forEach((s) -> System.out.println(s));—list.forEach(Syetem.out::println);

  4. 函数式接口:有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。JDK 1.8 新增加的函数接口:

    · java.util.function

  5. Stream:

  6. Optional类

  7. 新时间日期API:API LocalDate | LocalTime | LocalDateTime


如何理解面向对象和面向过程?面向过程的优缺点

面向过程

优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,因为性能对他们来说是最重要的因素。

缺点:没有面向对象易维护、易复用、易扩展。

面向对象

优点:易维护、易复用、易扩,由于面向对象有封装、继承和多态的特性,故可以设计出低耦合的系统,使系统更加灵活更加易于维护。

缺点:性能比面向过程低。

面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。面向对象是一种以事物为中心的编程思想,注重的是对象本身

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。面向过程就是以过程为中心的编程思想,注重的是实现程序这个过程


(20)Synchronized

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");
        }
    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

(21)常量池

Java文件被编译成class文件,class文件中处理有类的版本、字段、方法、接口等信息外,还有一个常量池表。(运行时常量池)常量池表用来存放编译期生成的各种字面量及符号引用。

常量池:

  1. 在堆中的字符串常量池
    • 字符串常量池由String类私有维护
  2. 在方法区的运行时常量池(每一个类都有一个运行时常量池)
    • 各种字面量
    • 符号引用
在这里插入图片描述 在这里插入图片描述

(22)Java为什么可以跨平台

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEDtnUaa-1628430815575)(C:\Users\wei\AppData\Roaming\Typora\typora-user-images\image-20210429155422219.png)]

(23)final关键字

  1. 修饰变量
    • 修饰基本数据类型变量时:必须赋初值且不能被改变
    • 修饰引用变量:该引用变量不能在指向其它对象
    • static final ----> 常量
  2. 修饰方法
    • final方法不能被子类重写
    • final方法比非final方法快,在编译时就已经静态绑定,不需要在运行时再动态绑定
  3. 修饰类
    • final类不能被继承(如String,Integer以及其它包装类)
    • final类一般是功能完整的

(24)Java三大特性

1. 封装

封装就是把同一类事物的共性(包括属性和方法)归到同一类中,方便使用。

封装指的是属性私有化,根据需要提供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修饰,或者封装进方法内,使其不能直接被访问,增加了访问步骤与难度!

2. 继承

继承是指将多个相同的属性和方法提取出来,新建一个父类。
Java中一个类只能继承一个父类,且只能继承访问权限非private的属性和方法。 子类可以重写父类中的方法,命名与父类中同名的属性。子类可以直接访问父类的非私有化成员变量,访问父类的私有化成员变量可以使用super.get()方法。

继承目的:代码复用。

3. 多态

多态可以分为两种:设计时多态和运行时多态。

  • 设计时多态:即重载,是指Java允许方法名相同而参数不同(返回值可以相同也可以不相同)。
  • 运行时多态:即重写,是指Java运行根据调用该方法的类型决定调用哪个方法。

我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。这也是为什么有时候多态方法又被称为延迟方法的原因。

(25)HashSet

直接看HashMap

1. 底层实现

HashSet的底层实现是HashMap

Set不能有重复的元素,HashMap不允许有重复的键

  • Set中有且只有1个元素的值为null

  • HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。

在HashSet中

  • 元素都存到HashMap键值对的Key上面,
  • Value是有一个统一的值private static final Object PRESENT = new Object();
    • 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。

2. 源码分析

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();
    }
......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

3. HashSet的插入与删除

插入:

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
  • 1
  • 2
  • 3

当有新值加入时,底层的HashMap会判断Key值是否存在(HashMap细节请移步深入理解HashMap

  • 如果不存在,则插入新值,同时这个插入的细节会依照HashMap插入细节
  • 如果存在就不插入

删除

    public boolean remove(Object o) {  
    return map.remove(o)==PRESENT;  
    }  
  • 1
  • 2
  • 3

同HashMap删除原理:底层实际调用HashMap的remove方法删除指定Entry。

  • 如果存在于此set中,则需要将其移除的对象。,并返回true
  • 如果不存在,返回false

【问题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。

注意

  • 说白了,HashSet就是限制了功能的HashMap,所以了解HashMap的实现原理,这个HashSet自然就通
  • 对于HashSet中保存的对象,主要要正确重写equals方法和hashCode方法,以保证放入Set对象的唯一性
  • 虽说是Set是对于重复的元素不放入,倒不如直接说是底层的Map直接把原值替代了(这个Set的put方法的返回值真有意思)
  • HashSet没有提供get()方法,原因是同HashMap一样,Set内部是无序的,只能通过迭代的方式获得

(26)equals()&& hashCode()

为什么重写equals()方法也要重写hashCode()方法?

  1. 使用hashcode方法提前校验,可以避免每一次比对都调用equals方法,提高效率

  2. 保证是同一个对象。如果重写了equals方法,而没有重写hashcode方法,会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。(在HashMap中会可能会将equals()返回true的对象存为不同的元素,因为不同的对象会被判断为不同的key,分开存储)

    • 为了保证同一个对象,保证在equals相同的情况下hashcode值必定相同,如果重写了equals而未重写hashcode方法,可能就会出现两个没有关系的对象equals相同的(因为equal都是根据对象的特征进行重写的),但hashcode确实不相同的。
image-20210528093209975

并不是存失败的意思,而是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。

注意

  • 说白了,HashSet就是限制了功能的HashMap,所以了解HashMap的实现原理,这个HashSet自然就通
  • 对于HashSet中保存的对象,主要要正确重写equals方法和hashCode方法,以保证放入Set对象的唯一性
  • 虽说是Set是对于重复的元素不放入,倒不如直接说是底层的Map直接把原值替代了(这个Set的put方法的返回值真有意思)
  • HashSet没有提供get()方法,原因是同HashMap一样,Set内部是无序的,只能通过迭代的方式获得

(26)equals()&& hashCode()

为什么重写equals()方法也要重写hashCode()方法?

  1. 使用hashcode方法提前校验,可以避免每一次比对都调用equals方法,提高效率

  2. 保证是同一个对象。如果重写了equals方法,而没有重写hashcode方法,会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。(在HashMap中会可能会将equals()返回true的对象存为不同的元素,因为不同的对象会被判断为不同的key,分开存储)

    • 为了保证同一个对象,保证在equals相同的情况下hashcode值必定相同,如果重写了equals而未重写hashcode方法,可能就会出现两个没有关系的对象equals相同的(因为equal都是根据对象的特征进行重写的),但hashcode确实不相同的。
image-20210528093209975
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/325127
推荐阅读
相关标签
  

闽ICP备14008679号