JavaSE面试相关

区别

重载与重写

重载: 发生在同一个类中,方法名必须相同,参数类型不同.个数不同.顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。

重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。

成员变量与局部变量

区别成员变量局部变量
类中位置不同类中,方法外常见于方法中
初始化值不同有默认值, 不需要初始化赋值没有默认值,使用之前必须完成赋值
内存位置不同堆内存栈内存
作用域不同整个对象在所归属的大括号中

基本类型和包装类型的区别

  • 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
  • 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
  • 默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null
  • 比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。

记住:所有整型包装类对象之间值的比较,全部使用 equals 方法比较

集合和数组的区别

  • 长度 :
    数组的长度[固定的]
    集合的长度[可变的]

  • 存储 :
    数组: 可以存储基本数据类型, 也可以存储引用数据类型,但必须是同一类型的元素,而集合只能存储对象,且可以存储不同类型的数据。

  • 访问:

    数组可以进行快速的随机访问,因为它是通过索引直接访问元素的。但是,集合的访问速度相对较慢,因为需要进行遍历来查找元素。

String家族的区别

类型可变性线程安全性效率适用场景
String不可变线程安全低(每次操作创建新对象)不经常更改的字符串
StringBuffer可变线程安全中(使用同步方法)多线程环境或频繁更改的字符串
StringBuilder可变非线程安全单线程环境或频繁更改的字符串
StringJoiner可变非线程安全字符串分隔符拼接的操作

接口与抽象类区别

构造函数:抽象类可以有构造函数;接口不能有。

main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。

访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。

JDK、JRE、JVM

在这里插入图片描述

JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,是整个 java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的软件程序。是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

JRE(Java Runtime Environment)是运行 JAVA运行时环境,是运行已编译 Java 程序所需的所有内容的集合,包含 java 虚拟机和 java 基础的核心类库。

JDK(Java Development Kit)是整个 Java 的核心,它是功能齐全的 Java SDK,是提供给开发者使用的,能够创建和编译 Java 程序。包括了 Java 运行环境 JRE、Java 工具和 Java 基础类库。

什么是字节码?采用字节码的好处是什么?

在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

Java 程序从源代码到运行的过程如下图所示

在这里插入图片描述

我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(just-in-time compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存内存中,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言

在这里插入图片描述

下面这张图是 JVM 的大致结构模型。

在这里插入图片描述

为什么说 Java 语言“编译与解释并存”?

其实这个问题我们讲字节码的时候已经提到过,因为比较重要,所以我们这里再提一下。

我们可以将高级编程语言按照程序的执行方式分为两种:

  • 编译型编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
  • 解释型解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。

在这里插入图片描述

根据维基百科介绍:

为了改善编译语言的效率而发展出的即时编译技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。JavaLLVM是这种技术的代表产物。

相关阅读:基本功 | Java 即时编译器原理解析及实践

为什么说 Java 语言“编译与解释并存”?

这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行。

Java 中==和 equals 的区别(必会)

== 的作用:

基本类型:比较的就是值是否相同

引用类型:比较的就是地址值是否相同

equals 的作用: 引用类型:默认情况下,比较的是地址值。

特:String、Integer、Date 这些类库中 equals 被重写,比较的是内容而不是地址!

Hashmap 和 hashtable ConcurrentHashMap 区别(高薪常问)

区别对比一(HashMap 和 HashTable 区别):

1、HashMap 是非线程安全的,HashTable 是线程安全的。

2、HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行。

3、因为线程安全的问题,HashMap 效率比 HashTable 的要高。

4、Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线程环境,而 Hashtable 适合于多线程环境。一般现在不建议用 HashTable, ①是 HashTable 是遗留类,内部实现很多没优化和冗余。②即使在多线程环境下,现在也有同步的ConcurrentHashMap 替代,没有必要因为是多线程而用HashTable。

区别对比二(HashTable 和 ConcurrentHashMap 区别):

HashTable 使用的是 Synchronized 关键字修饰,ConcurrentHashMap 是JDK1.7 使用了锁分段技术来保证线程安全的JDK1.8ConcurrentHashMap 取消了Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8的结构类似,数组+链表/红黑二叉树。

synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。

并发并行

并发:同一时间段,CPU分时轮询的执行线程。

并行:同一个时刻,同时在执行。

javase基础

运算符

  • Java程序中支持书写二进制、八进制、十六进制的数据,分别需要以0B或者0b、0、0X或者0x开头
  • 类名、变量名都是标识符,不能以数字开头,只能由数字、字母、_和$组成,不能用关键字
  • 在表达式中,byte、short、char 是直接转换成int类型参与运算的。就算是byte与short进行运算,结果也是int。这是java的常量优化机制
  • switch的表达式类型只能是byte、short、int、char,JDK5开始支持枚举,JDK7开始支持String、不支持double、float、long,因为有精度问题
  • 数组等于null和等于{}是不一样的 前者调用arr.length报错 后者为0

移位运算符

移位运算符是最基本的运算符之一,几乎每种编程语言都包含这一运算符。移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。

Java 中有三种移位运算符:

  • << :左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。
  • >> :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。
  • >>> :无符号右移,忽略符号位,空位都以 0 补齐。

移位操作符实际上支持的类型只有intlong,编译器在对shortbytechar类型进行移位前,都会将其转换为int类型再操作。

数组

初始化

  • 静态初始化

    数据类型[] 数组名 = { 元素1,元素2 ,元素3,… };

    int[] ages = {12, 24, 36, 48, 60};

    数据类型[] 数组名 = new 数据类型[]{ 元素1,元素2 ,元素3… };

    int[] ages = new int[]{12, 24, 36, 48, 60};

数据类型[] 数组名 也可写成 数据类型 数组名[]

  • 动态初始化:

    数据类型[] 数组名 = new 数据类型[长度];

    int[] ages = new int[4];

内存分配

方法区:字节码文件加载到这里,即.class文件

栈:方法运行时所进入的内存、局部变量、数组名

堆:new出来的东西在这里开辟空间,对象、产生地址、成员变量、数组对象元素

  • 堆内存中的对象,没有被任何变量引用(指向)时,就会被判定为内存中的“垃圾”。

javase进阶

类与对象与接口

类与对象

  • 一旦定义了有参构造器,Java就不会帮我们的类生成无参构造器了此时就建议自己手写一个无参构造器出来了

  • 面向对象的特征:封装、继承、多态、抽象

    • 封装:就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对
      象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的
      公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性。

    • 继承:子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提
      高了代码的复用性

      • 子类的全部构造器,都会先调用父类的构造器,再执行自己
      • this(…) 、super(…) 都只能放在构造器的第一行,因此,有了this(…)就不能写super(…)了
    • 多态:多态是在继承/实现情况下的一种现象,表现为:对象多态、行为多态。在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法

      • 多态的前提

        有继承/实现关系;

        存在父类引用子类对象;即Person p =new Student();

        存在方法重写。

      • 多态下不能使用子类的独有功能,必须强转才行

      • 使用instanceof关键字,判断当前对象的真实类型,再进行强转。object instanceof ClassName

        其中,object是要测试的对象,ClassName是要测试的类或接口的名称。如果objectClassName的实例(或者是其子类的对象实例),则返回true,否则返回false

    • 抽象:abstract修饰类、方法

      • 抽象类的注意事项、特点

        • 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。
        • 类该有的成员(成员变量、方法、构造器)抽象类都可以有。
        • 抽象类最主要的特点:抽象类不能创建对象,仅作为一种特殊的父类,让子类继承并实现。
        • 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。

接口

接口的成员变量默认为常量,方法默认为抽象方法

接口能被多继承,实现类必须重写完全部接口的全部抽象方法,否则实现类需要定义成抽象类

jdk8后接口中新增三种形式方法

  1. public default
  2. private 只能在此接口内调用
  3. public static

匿名内部类

就是一种特殊的局部内部类(局部内部类是定义在方法、代码块、构造器等执行体中的类);所谓匿名:指的是程序员不需要为这个类声明名字。

匿名内部类本质就是一个子类,并会立即创建出一个子类对象,通常作为一个参数传输给方法。

Person p = new Person(){//此类即为一个子类,继承Person类
		@override
    重写的方法
}

String、StringBuffer、StringBuilder

三者区别见第一章区别

String创建对象注意事项

String对象的内容不可改变,被称为不可变字符串对象。

  • 使用String str = “dhdh”; 创建对象的时候会默认把""内的东西放到堆内存区中的字符串常量池,相同内容的字符串只存储一份;
  • 除了""创建方式外,其他任何方法都是放在堆内、常量池外的区域内:通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存、常量池外;使用+=每次也都是产生一个新对象到堆内存、常量池外
  • String s2 = new String(“abc”);会创建两个对象,一个是放在常量池中的"abc",一个是放在池外的"abc",地址不同。
    • 若再String s3 = new String(“abc”);,则s3也!=s2
  • Java存在编译优化机制,程序在编译时(真正运行前): “a” + “b” + “c” 会直接转成 “abc”,以提高程序的执行性能,也即String s1 = “abc”; String s2 = “a” + “b” + “c”;两句话在执行时是一模一样的意思
    • 如String s1 = “abc”; String s2 = “a” + “b” + “c”; System.out.println(s1 == s2);结果为true

字符串对象的常用方法

方法名说明
public int length()获取字符串的长度返回(就是字符个数)
public char charAt(int index)获取某个索引位置处的字符返回
public char[] toCharArray():将当前字符串转换成字符数组返回
public boolean equals(Object anObject)判断当前字符串与另一个字符串的内容一样,一样返回true
public boolean equalsIgnoreCase(String anotherString)同上但忽略大小写
public String substring(int beginIndex, int endIndex)根据开始和结束索引进行截取,得到新的字符串(包前不包后
public String substring(int beginIndex)从传入的索引处开始截取,截取到末尾,得到新的字符串返回
public String replace(CharSequence target, CharSequence replacement)使用新值,将字符串中的旧值替换,得到新的字符串
public boolean contains(CharSequence s)判断字符串中是否包含了某个字符串
public boolean startsWith(String prefix)判断字符串是否以某个字符串内容开头,开头返回true,反之
public String[] split(String regex)把字符串按照某个字符串内容分割,并返回字符串数组回来
indexOf()返回指定字符的索引。
trim()去除字符串两端空白。
getBytes()返回字符串的 byte 类型数组

StringBuilder

是一个容器,用于操作字符串的类,其保存的字符串可以进行内容的修改,因此StringBuilder比String更适合做字符串的修改操作,效率会更高,代码也会更简洁。

常用方法

方法名称说明
public StringBuilder append(任意类型)添加数据并返回StringBuilder对象本身
public StringBuilder reverse()将对象的内容反转
public int length()返回对象内容长度
public String toString()通过toString()就可以实现把StringBuilder转换为String

StringJoiner

也是一个容器,是专门用于添加间隔字符操作的类,内容可变,在有些场景下使用它操作字符串,代码会更简洁

构造器说明
public StringJoiner (间隔符号)创建一个StringJoiner对象,指定拼接时的间隔符号
public StringJoiner (间隔符号,开始符号,结束符号)创建一个StringJoiner对象,指定拼接时的间隔符号、开始符号、结束符号
方法名称说明
public StringJoiner add (添加的内容)添加数据,并返回对象本身
public int length()返回长度 ( 字符出现的个数)
public String toString()返回一个字符串(该字符串就是拼接之后的结果)

总结

String 中的 String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[] ,String 对象是不可变的,也就可以理解为常量,线程安全。

StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 没有。

关键字

  • 静态代码块(加static关键字)在类加载时自动执行,实例代码块在每次创建对象时执行,并在构造器前执行

  • final是最终的意思,只能被赋值一次且要在初始化就赋值,之后就不能更改,可以修饰(类、方法、变量)

    • 修饰类、方法、变量
      • final修饰的引用类型,地址不能更改,但地址所指向的数据体内的数据可以更改,如final int arr1 ={1,2,3}; arr1[0] = 2 是允许的
  • 使用了 static final 修饰的成员变量就被称为常量

    • 常量名的命名规范:建议使用大写英文单词,多个单词使用下划线连接起来。
    • 程序编译后,常量会被“宏替换”:出现常量的地方全部会被替换成其记住的字面量,这样可以保证使用常量和直接用字面量的性能是一样的。
  • final和static的区别

    • final 用于限制变量、方法或类的可变性,使其不可修改、不可覆盖或不可继承。
    • static 用于创建类级别的成员,可以在类的所有实例之间共享,也用于定义静态方法和静态内部类。

七、集合

集合主要分为Collection和Map两种,前者是单列集合,后者是双列集合

集合中存储的是元素对象的地址

Collection

在这里插入图片描述

遍历方式

1.迭代器(存在并发修改异常问题)

使用Collection对象.iterator()来返回迭代器对象,

方法名称说明
Iterator iterator()返回集合中的迭代器对象,该迭代器对象默认指向当前集合的第一个元素

迭代器对象的方法

方法名称说明
boolean hasNext()询问当前位置是否有元素存在,存在返回true ,不存在返回false
E next()获取当前位置的元素,**并同时将迭代器对象指向下一个元素处。**不要用多了
remove()删除迭代器当前遍历的数据,且在底层做了i–操作(避免并发修改异常,若使用list.remove就会报错)
 Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String str = it.next();
            if ("B".equals(str)) {
                it.remove(); // 使用迭代器的 remove() 方法删除元素
            }
        }

2.增强for循环(存在并发修改异常问题)

增强for遍历集合,本质就是迭代器遍历集合的简化写法

3.lambda foreach(存在并发修改异常问题)

本质是迭代器遍历集合

在这里插入图片描述

集合的并发修改异常

使用迭代器遍历集合时,又同时在删除集合中的数据,程序就会出现并发修改异常的错误。

解决:

  • 使用迭代器遍历集合,但用迭代器自己的删除方法(iterator.remove)删除数据即可。
  • 用for循环遍历:可以倒着遍历并删除;或者从前往后遍历,但删除元素后做i --操作。

ArrayList

  • 底层:基于数组实现的。

    • 利用无参构造器创建的集合,会在底层创建一个默认长度为0的数组
    • 添加第一个元素时,底层会创建一个新的长度为10的数组
    • 存满时,会扩容1.5倍
      • 数组扩容,并不是在原数组上扩容(原数组是不可以扩容的),底层是创建一个新数组,然后把原数组中的元素全部复制到新数组中去。
    • 如果一次添加多个元素,1.5倍还放不下,则新创建数组的长度以实际为准
  • 查询速度快(注意:是根据索引查询数据快):查询数据通过地址值和索引定位,查询任意数据耗时相同。

  • 删改效率低,因为数组存在数据的移动

LinkedList

  • 底层:基于双向链表实现的。
  • 查询慢,但对首尾元素进行增删改查的速度是极快的,删改快

新增很多首尾操作的特有方法,因为对首尾元素增删改查极快,可以实现队列、栈(可以new完LinkedList对象,直接用对象名调用pop push)

Set

Set要用到的常用方法,基本上就是Collection提供的

Hash值

  • Hash值就是一个int类型的数值,Java中每个对象都有一个哈希值
  • Java中的所有对象,都可以调用Obejct类提供的hashCode方法,返回该对象自己的哈希码值。
  • 同一个对象多次调用hashCode()方法返回的哈希值是相同的。
  • 不同的对象,它们的哈希值一般不相同,但也有可能会相同(哈希碰撞)。

HashSet

无序但不是随机

底层原理

  • 基于哈希表实现。哈希表是一种增删改查数据,性能都较好的数据结构。

  • 哈希表JDK8之前,哈希表 = 数组+链表。JDK8开始,哈希表 = 数组+链表+红黑树

    • 创建一个默认长度16的数组,默认加载因子为0.75,数组名table

      • 加载因子:即数组中存放的元素/总长,当数组总元素超过0.75*16的时候,需要扩容,扩容就直接扩两倍
    • 使用元素的哈希值对数组的长度求余计算出应存入的位置

    • 判断当前位置是否为null,如果是null直接存入

    • HashSet是**采用拉链法(一个数组元素内容为一个链表)**解决哈希冲突的

    • 如果不为null,表示有元素则调用equals方法比较属性值(如果是对象,则参考下面的去重机制),若相等,则不存;不相等,则存入数组

      • JDK 8之前,新元素存入数组,占老元素位置,老元素挂下面

        • 在这里插入图片描述
      • JDK 8开始之后,新元素直接挂在老元素下面

    • JDK8开始,当链表长度超过8,且数组长度>=64时,自动将链表转成红黑树

      • 红黑树暂时就记为有序的平衡二叉树
  • 去重机制:

    • HashSet集合默认不能对内容一样的两个不同对象去重复! 比如内容一样的两个学生对象存入到HashSet集合中去 , HashSet集合是不能去重复的!需要重写对象Student的hashCode()和equals()方法

LinkedHashSet

底层

  • 有序是因为:依然是基于哈希表(数组、链表、红黑树)实现的。但是,它的每个元素都额外的多了一个双链表的机制记录它前后元素的位置。

TreeSet

可排序,默认升序

底层

  • 基于红黑树实现的排序。

  • 对于自定义类型如Student对象,TreeSet默认是无法直接排序的。需要自定义排序规则

    • 方式一
      • 让自定义的类(如学生类)实现Comparable接口,重写里面的compareTo方法来指定比较规则。
    • 方式二
      • 通过调用TreeSet集合有参数构造器,可以设置Comparator对象(比较器对象,用于指定比较规则。即new TreeSet()的时候在括号内new Comparator来重写方法
      • TreeSet默认选择自己自带的比较器进行比较,也就是方法二
  • 去重原理:是根据compareTo返回0,说明是重复的

应用场景总结

1、如果希望记住元素的添加顺序,需要存储重复的元素,又要频繁的根据索引查询数据?用ArrayList集合(有序、可重复、有索引),底层基于数组的。(常用)

2、如果希望记住元素的添加顺序,且增删首尾数据的情况较多?用LinkedList集合(有序、可重复、有索引),底层基于双链表实现的。

3、如果不在意元素顺序,也没有重复元素需要存储,只希望增删改查都快?用HashSet集合(无序,不重复,无索引),底层基于哈希表实现的。 (常用)

4、如果希望记住元素的添加顺序,也没有重复元素需要存储,且希望增删改查都快?用LinkedHashSet集合(有序,不重复,无索引), 底层基于哈希表和双链表。

5、如果要对元素进行排序,也没有重复元素需要存储?且希望增删改查都快?用TreeSet集合,基于红黑树实现。

Collections

是一个用来操作集合的工具类

Collections只能支持对List集合进行排序

Collections提供的常用静态方法如下

方法名称说明
public static boolean addAll(Collection<? super T> c, T… elements)给集合批量添加元素
public static void shuffle(List<?> list)打乱List集合中的元素顺序
public static void sort(List list)对List集合中的元素进行升序排序(自定义类型必须实现了Comparable接口)
public static void sort(List list,Comparator<? super T> c)List集合中元素,按照比较器对象指定的规则进行排序

Map

Map集合的所有键是不允许重复的,但值可以重复

在这里插入图片描述

(特点同xxxSet集合,底层原理:Set集合是基于Map集合来的,只是Set集合中的元素只要键数据,不要值数据而已。所以一样)

Map常用方法

方法名称说明
public V put(K key,V value)添加元素
public int size()获取集合的大小
public void clear()清空集合
public boolean isEmpty()判断集合是否为空,为空返回true , 反之
public V get(Object key)根据键获取对应值
public V remove(Object key)根据键删除整个元素
public boolean containsKey(Object key)判断是否包含某个键
public boolean containsValue(Object value)判断是否包含某个值
public Set keySet()获取全部键的集合
public Collection values()获取Map集合的全部值
Set<Map.Entry<K, V>> entrySet()获取所有“键值对”的集合
default void forEach(BiConsumer<? super K, ? super V> action)结合lambda遍历Map集合
Map.Entry提供的方法说明
K getKey()获取键
V getValue()获取值
//entrySet方法
Set< Map.Entry<String, Double > entries = map.entrySet();//获得键值对

//根据entry对象来获得键值
for (Map.Entry<String, Double> entry : entries) { 
    String key = entry.getKey(); 
    double value = entry.getValue();
    System.out.println(key + "====>" + value);
}
//forEach方法遍历
map.forEach((k , v) -> {
    System.out.println(k +"----->" + v);
});//lambda表达式省略了new BiConsumer的步骤

HashMap

非线程安全,高效,支持 null 值和 null 键

HashMap跟HashSet的底层原理是一模一样的,都是基于数组+链表的哈希表实现的。

同样是在 JDK1.8 后对 HashMap 进行了底层优化,改为了由 数组+链表+红黑树实现,当链表中的元素超过了 8 个以后, 会将链表转换为红黑树,当红黑树节点 小于 等于 6 时又会退化为链表。主要的目的是提高查找效率。

往HashMap集合中键值对数据时,底层步骤如下
	第1步:当你第一次往HashMap集合中存储键值对时,底层会创建一个长度为16的数组
	第2步:把键然后将键和值封装成一个对象,叫做Entry对象
	第3步:再根据Entry对象的键计算hashCode值(和值无关)
	第4步:利用hashCode值和数组的长度做一个类似求余数的算法,会得到一个索引位置
	第5步:判断这个索引的位置是否为null,如果为null,就直接将这个Entry对象存储到这个索引位置
		   如果不为null,则还需要进行第6步的判断
	第6步:继续调用equals方法判断两个对象键是否相同
		  如果equals返回false,则以链表的形式往下挂
		  如果equals方法true,则认为键重复,此时新的键值对会替换就的键值对。
	
HashMap底层需要注意这几点:
	1.底层数组默认长度为16,如果数组中有超过12个位置已经存储了元素,则会对数组进行扩容2倍
	  数组扩容的加载因子是0.75,意思是:16*0.75=12 	
   	
    2.数组的同一个索引位置有多个元素、并且在8个元素以内(包括8),则以链表的形式存储
    	JDK7版本:链表采用头插法(新元素往链表的头部添加)
    	JDK8版本:链表采用尾插法(新元素往那个链表的尾部添加)
    	
    3.数组的同一个索引位置有多个元素、并且超过了8个,则以红黑树形式存储

从HashMap底层存储键值对的过程中我们发现:决定键是否重复依赖与两个方法,一个是hashCode方法、一个是equals方法。有两个键计算得到的hashCode值相同,并且两个键使用equals比较为true,就认为键重复。

所以,往Map集合中存储自定义对象作为键,为了保证键的唯一性,我们应该重写hashCode方法和equals方法。

LinkedHashMap

线程不安全,是 HashMap 的一个子类,保存了记录的插入顺序;

底层数据结构依然是基于哈希表实现的,只是每个键值对元素又额外的多了一个双链表的机制记录元素顺序(保证有序)。

TreeMap

特点:不重复、无索引、可排序(按照键的大小默认升序排序,只能对键排序)

底层原理:TreeMap跟TreeSet集合的底层原理是一样的,都是基于红黑树实现的排序。

HashTable

线程安全,低效,不支持 null 值和 null 键;

集合总结

Collection集合名底层特点
ArrayList数组有序、有重复、有索引 ,非线程安全
LinkedList链表有序、有重复、有索引,非线程安全
HashSet哈希表无序、无重复、无索引
LinkedHashSet哈希表+双向链表有序、无重复、无索引
TreeSet红黑树排序、无重复、无索引
Vector数组 ,查询快,增删慢线程安全
Map集合名
HashMap哈希表无序、无重复、无索引,非线程安全
LinkedHashMap哈希表+双向链表有序、无重复、无索引,非线程安全
TreeMap红黑树排序、无重复、无索引,非线程安全
HashTable线程安全

设计模式

饿汉

线程安全

调用之前就已经创建好了对象

  1. 私有化构造器
  2. 定义一个private类方法来 new此对象来保存
  3. 提供一个类方法来供外部获得此类的对象
public class A { 
    // 2、定义一个类变量记住类的一个对象 
    private static A a = new A();
 // 1、私有构造器 
    private A(){ }
 // 3、定义一个类方法返回对象
    public static A getObject(){
        return a; 
    }
}

懒汉

非线程安全

调用的时候才创建对象

  1. 私有化构造器
  2. 定义一个private类方法来保存对象,但不new
  3. 定义一个类方法返回对象,首次则new,否则直接返回
public class A { 
    // 2、定义一个类变量记住类的一个对象 
    private static A a;
 // 1、私有构造器 
    private A(){ }
 // 3、定义一个类方法返回对象
    public static A getObject(){
        if(a == null){
            a = new A();
        }      
        return a; 
    }
}

双检锁:线程安全,延迟初始化。

public class A { 
    // 2、定义一个类变量记住类的一个对象 
    private volatile static A a;
 // 1、私有构造器 
    private A(){ }
 // 3、定义一个类方法返回对象
    public static A getObject(){
        if(a == null){
            synchronized (A.class){
               if(a == null){
                a = new A();
               }
            }   
        }      
        return a; 
    }
}

模板方法设计模式

模板方法设计模式的写法

1、定义一个抽象类。

2、在里面定义2个方法

  • 一个是模板方法:把相同代码放里面去,在此方法内调用抽象方法以实现子类对模板方法的不同实现。
  • 一个是抽象方法:具体实现交给子类完成。

建议使用final关键字修饰模板方法,因为一旦子类重写了模板方法,模板方法就失效了

权限修饰符

权限修饰符就是是用来限制类中的成员(成员变量、成员方法、构造器、代码块…)能够被访问的范围。

修饰符本类里同一个包中的类任意包下的子孙类任意类
private
缺省
protected
public

若在任意包下的一个非子孙类C里,定义了一个A的子类B,则在C下的程序中,B无法访问A中的protected成员,只有在B类中的程序中才能访问A的protected成员

泛型

泛型的主要作用是为了在编译时限制类或方法可以操作的数据类型,从而避免在运行时出现类型转换错误。

泛型是工作在编译阶段的,一旦程序编译成class文件,class文件中就不存在泛型了,这就是泛型擦除

把具体的数据类型作为参数传给类型变量,泛型只能用引用类型,不能用基础数据类型如int

泛型的上下限

  • 泛型上限: E extends Car: E能接收的必须是Car或者其子类 。
  • 泛型下限: E super Car : E能接收的必须是Car或者其父类。

通配符:就是 “?” ,可以在“使用泛型”的时候代表一切类型,

泛型方法

public T add(T t) 并不是泛型方法,因为这里的T不是这个泛型方法自己定义的,这里只是在参数列表中使用了泛型。public <T> void add(T t)才是泛型方法,这个是自己声明的

泛型方法主要用于当方法参数列表中出现ArrayList的时候,补充定义在方法声明里,这是因为比如参数列表为ArrayList,而实际传入的是ArrayList的时候会报错,因为BMW虽然是Car的子类,但是ArrayList和ArrayList还是两个东西,不能让后者传入到前者里。而定义了之后就能用ArrayList作为参数列表从而支持把ArrayList传入参数列表中的ArrayList里

泛型方法的参数列表中一定要有此泛型方法定义的泛型类型

API工具类

Object

这里设自定义类为User u1 = new User();

clone 会复制一个一模一样新对象返回,调用User u2 = (User)u1.clone();之前,需要User类实现接口Cloneable(标记接口而已,没有什么方法需要实现),然后再User类内重写clone方法才能使用(不用手动重写,直接用生成的就行) 注意强制类型转换是必须要有的

  • 浅克隆(上述浅克隆)

    • 会将原对象引用类型的数据的地址值直接赋给新对象,也就是两个对象中的数据地址一样,没有new新的引用成员变量,正常引用类型拷贝的只是地址而非数据
  • 深克隆

    • 对象中包含的其他对象,不会拷贝地址,会创建新对象。(需要手动实现Cloneable接口中的clone方法,并且重写)

    • @Override
          protected Object clone() throws CloneNotSupportedException {
              //先克隆得到一个新对象
              User u = (User) super.clone();
              //再将新对象中的引用类型数据,再次克隆
              u.scores = u.scores.clone();
              return u;//浅克隆的时候没有上面两行,直接return super.clone();
          }
      

包装类

针对基本数据类型和包装类的转换

  • 自动装箱:将基本类型用它们对应的引用类型包装起来;
  • 自动拆箱:将包装类型转换为基本数据类型;

装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。

  • Integer i = 10 等价于 Integer i = Integer.valueOf(10)
  • int n = i 等价于 int n = i.intValue();

注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。

针对字符串和基本数据类型的转换

  • 包装类名.toString:基本数据类型—>字符串 String str = Double.toString(3.9);
  • 包装类名.parseInt(更常用的是包装类名.valueof(int)):字符串—>基本数据类型
    • 注意使用valueof(str1)的时候返回值一定要跟str1应该对应的类型一致,比如str1是double类型,如果用int来接收返回值则会出bug

BigDecimal

用于解决浮点型运算时,出现结果失真的问题。

结果失真:float使用二进制来表示浮点数,这导致一些十进制小数无法被精确表示。例如,0.1(十进制)在二进制中是一个无限循环小数。

构造器说明
public BigDecimal(double val) 注意:不推荐使用这个,推荐valueof,在下下面将 double转换为 BigDecimal
public BigDecimal(String val)把String转成BigDecimal
方法名说明
public static BigDecimal valueOf(double val)转换double成 BigDecimal
public BigDecimal add(BigDecimal b)加法
public BigDecimal subtract(BigDecimal b)减法
public BigDecimal multiply(BigDecimal b)乘法
public BigDecimal divide(BigDecimal b)除法
public BigDecimal divide (另一个BigDecimal对象,精确几位,舍入模式) (RoundingMode.HALF_UP为四舍五入)除法、可以控制精确到小数几位
public double doubleValue()将BigDecimal转换为double

Arrays

用来操作数组的一个工具类。以下静态方法都直接用Arrays.方法名来调用即可,不用新建Arrays对象

方法名说明
public static String toString(类型[] arr)返回数组的内容
public static int[] copyOfRange(类型[] arr, 起始索引, 结束索引)拷贝数组(指定范围)
public static copyOf(类型[] arr, int newLength)拷贝数组
public static setAll(double[] array, IntToDoubleFunction generator)把数组中的原数据改为新数据
public static void sort(类型[] arr)对数组进行排序(默认是升序排序)

sort

若数组中存储的是对象元素,则直接用Arrays.sort则报错,有两种方法解决

  • 让该对象的类实现Comparable(比较规则)接口,然后重写compareTo方法,自己来制定比较规则。

  • 使用下面这个sort方法,创建Comparator比较器接口的匿名内部类对象,然后自己制定比较规则,实现compare方法。

    • public static void sort(T[] arr, Comparator<? super T> c)对数组进行排序(支持自定义排序规则)
    • 使用以上方法的时候,需要遵循的官方约定如下

      如果认为左边对象 大于 右边对象 应该返回正整数,

      如果认为左边对象 小于 右边对象 应该返回负整数

      如果认为左边对象 等于 右边对象 应该返回0整

      比较结果:使用(o1,o2)->o1.getAge-o2.getAge会把小的那个对象放前面,即默认升序排列,若用o2.getAge-o1.getAge则会降序排列

日期

JDK8之前传统的时间API(Date、SimpleDateFormat、Calendar),都是可变对象,修改后会丢失最开始的时间信息,线程不安全。jdk8之后的是不可变对象,线程安全。

jdk8之后的新增日期类,在获取当前对象的时候用类名.now或类名.of

在这里插入图片描述

正则表达式

public boolean matches(String regex)判断字符串是否匹配正则表达式,匹配返回true,不匹配返回false。
String提供一个匹配正则表达式的方法

regex书写规则

在这里插入图片描述

下面表中举例的\d实际是\\d,md文字的显示问题而已

符号含义举例
?0次或1次\d?
*0次或多次\d* (abc)*
+1次或多次\d+ (abc)+
{}具体次数,放在数据后a{7} \\d{7,19}(7~19个0-9的数字){2,}表示2位及以上
(?i)忽略后面字符的大小写(?i)abc
a((?i)b)c只忽略b的大小写a((?i)b)c
符号含义举例
[]里面的内容出现一次[abc] ( [3-9]即只能是3~9中的数字)
^取反[^abc]
&&交集,不能写单个的&[a-z&&m-p]
.任意字符\n 回车符号不匹配
\转义字符\\d
\d0-9\d+
\D非0-9\D+
\s空白字符
\S非空白字符[^\s]
\w单词字符[a-zA-Z_0-9](下划线也包括)
\W非单词字符[^\w]
()分组a(bc)+
|写在方括号外面表示并集ab|AB

从一段文字中爬取指定文本的固定格式:

String regex = "\\w{2,}@\\w{2,}\\.(\\w{2,10}){1,2}";
        //下面这是从一段文字中爬取指定文本的固定格式
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(data);
        while (matcher.find()){
            System.out.println(matcher.group());
        }

正则表达式用于搜索替换、分割内容,需要结合String提供的如下方法完成:

方法名说明
public String replaceAll(String regex , String newStr)按照正则表达式匹配的内容进行替换
public String[] split(String regex):按照正则表达式匹配的内容进行分割字符串,反回一个字符串数组。

可变参数

就是一种特殊形参,定义在方法、构造器的形参列表里,格式是:数据类型…参数名称;

特点:可以传0个或者同时传多个数据给它;也可以传一个数组给它。好处:常常用来灵活的接收数据。

可变参数的注意事项:

  • 可变参数在方法内部就是一个数组
  • 一个形参列表中可变参数只能有一个
  • 可变参数必须放在形参列表的最后面,放前面的话,就不知道另一个参数从什么时候开始了

多线程

  • Thread.sleep() 和 Object.wait(),都可以抛出 InterruptedException。这个异常是不能忽略的,因为它是一个检查异常(checked exception)

什么是线程?线程和进程的区别?

线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。

进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。

特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间 内存共享,这使多线程编程可以拥有更好的性能和用户体验。

启动线程必须是调用start方法,不是调用run方法。直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。只有调用start方法,会让 jvm 调用 run 方法在开启的线程中执行,才是启动一个新的线程执行。

创建线程有几种方式

1.继承 Thread 类并重写 run 方法创建线程,实现简单但不可以继承其他类

public class MyThread extends Thread{
    // 2、必须重写Thread类的run方法
    @Override
    public void run() {
        // 描述线程的执行任务。
    }
}
public class ThreadTest1 {
    public static void main(String[] args) {
        // 3、创建MyThread线程类的对象代表一个线程
        Thread t = new MyThread();
        // 4、启动线程(自动执行run方法的)
        t.start(); 
    }
}

2.实现 Runnable 接口并重写 run。避免了单继承局限性,编程更加灵活,实现解耦。如果线程有执行结果是不能直接返回的。

/**
 * 1、定义一个任务类,实现Runnable接口
 */
public class MyRunnable implements Runnable{
    // 2、重写runnable的run方法
    @Override
    public void run() {
        // 线程要执行的任务。
    }
}
//测试类
public class ThreadTest2 {
	public static void main(String[] args) {
        // 3、创建任务对象。
        Runnable target = new MyRunnable();
        // 4、把任务对象交给一个线程对象处理。
        //  public Thread(Runnable target)
        new Thread(target).start();
    }
}

//用匿名内部类实现
 new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();

3.实现 Callable 接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常,支持泛型

1.先定义一个Callable接口的实现类,重写call方法
2.创建Callable实现类的对象
3.创建FutureTask类的对象,将Callable对象传递给FutureTask
4.创建Thread对象,将Future对象传递给Thread
5.调用Threadstart()方法启动线程(启动后会自动执行call方法)call()方法执行完之后,会自动将返回值结果封装到FutrueTask对象中
   
6.调用FutrueTask对的get()方法获取返回结果
class MyThread3 implements Callable{

//①创建一个类实现Callable接口,并重写里面的call()方法,实现打印线程名字和0-10
    @Override
    public Object call() throws Exception {
        //获得当前执行的线程对象
         Thread thread = Thread.currentThread();
        return thread.getName();
    }
}
public class test26_3 {
    public static void main(String[] args) {
        // ②创建一个测试类,在里面创建两个线程,
        FutureTask f1 = new FutureTask(new MyThread3());
        Thread thread1 = new Thread(f1);
        thread1.setName("一等奖");
        thread1.start();
        //调用FutrueTask对的get()方法获取返回结果
        System.out.println(f1.get(););
    }
}

4.使用线程池创建

线程生命周期(状态)

  1. 第一是 new->新建状态。在生成线程对象,并没有调用该对象的 start 方法,这是线程处于创建状态。

  2. 第二是 Runnable->就绪状态。当调用了线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。

  3. 第三是 Running->运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码

  4. 第四是阻塞状态。阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

(1)wait等待 – 通过调用线程的 wait() 方法,让线程等待某工作的完成。

(2)超时等待 – 通过调用线程的 sleep() 或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

(3)Blocked同步阻塞 – 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

  1. 第五是 dead->死亡状态: 线程run完了或者因异常退出了 run()方法,该线程结束生命周期.

线程相关的基本方法

线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等

1.线程等待(wait)

调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

2.线程睡眠(sleep)

sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态.

3.线程让步(yield)

yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。

4.线程中断(interrupt)

中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)

5.Join 等待其他线程终止

join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸.

6.线程唤醒(notify)

Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

Thread提供的常用方法说明
public void run()线程的任务方法
public void start()启动线程
public String getName()获取当前线程的名称,线程名称默认是Thread-索引
public void setName(String name)为线程设置名称
public static Thread currentThread()获取当前执行的线程对象
public static void sleep(long time)让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()…让调用当前这个方法的线程先执行完!
Thread提供的常见构造器说明
public Thread(String name)可以为当前线程指定名称
public Thread(Runnable target)封装Runnable对象成为线程对象
public Thread(Runnable target, String name)封装Runnable对象成为线程对象,并指定线程名称

wait()和 sleep()的区别?(必会)

1. 来自不同的类

wait():来自 Object 类;

sleep():来自 Thread 类;

2.关于锁的释放:

wait():在等待的过程中会释放锁;

sleep():在等待的过程中不会释放锁

3.使用的范围:

wait():因为要求线程释放锁,所以必须在同步代码块中使用,直到另一个线程调用相同对象的 notifynotifyAll 方法来唤醒它。

sleep():可以在任何地方使用;让当前线程暂停执行,通常用于模拟等待或延迟执行

4.是否需要捕获异常

wait():不需要捕获异常;

sleep():需要捕获异常;

线程安全

多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

线程同步是可以解决线程安全问题的方案,思想让多个线程实现先后依次访问共享资源,这样就解决了安全问题。常见方法是加锁,有三种方式来加锁

1.同步代码块

synchronized(同步锁) {
    访问共享资源的核心代码}

格式如上,其中对于当前同时执行的线程来说同步锁必须是同一把(同一个对象),否则会出bug,如使用"南宫"这个常量池里唯一的对象来讲是行的通,但是他把所有线程都锁住了,如果只用this来当同步锁对象(对于实例方法),则只会锁住几个线程对于同一个共享对象的操作,对于静态方法建议使用字节码(类名.class)对象作为锁对象。

  • 如一家两人共享一个银行卡号,两家人分别用两个银行卡号(两个共享资源),而用"南宫"作为同步锁的时候,会把这两家四个人都锁住,而用this则只锁一家人之内的银行卡号(共享资源)

2.同步方法

修饰符 synchronized 返回值类型 方法名称(形参列表) {	
    操作共享资源的代码}

格式如上,即对于操作共享资源的方法,声明上增加synchronized来修饰

同步方法底层也是用this或者类名.class

因为同步代码块锁的内容少一点,所以性能更好一点点,如一个方法中有1000行是没必要锁的,但是同步方法锁住了所有代码,导致这1000行代码变成近似单线程的操作了,速度慢

3.Lock锁

Lock是接口,不能直接实例化,需要自定义它的实现类ReentrantLock(暂时只用这个实现类)来构建Lock锁对象。

构造器说明
public ReentrantLock()获得Lock锁的实现类对象

Lock的常用方法

方法名称说明
void lock()加锁,在共享资源前用
void unlock()释放锁,在共享资源后用

线程池

创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。线程池就是一个可以复用线程的技术。

创建线程池

方式一:使用ExecutorService接口的实现类ThreadPoolExecutor自创建一个线程池对象。

方式二(不推荐):使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

  • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。缺乏配置选项,难以优化
    • 因为FixedThreadPool 和 SingleThreadPool的**方法内允许的请求队列长度为 Integer.MAX_VALUE,**可能会堆积大量的请求,从而导致 OOM。
    • 创建线程数量过大,导致OOM

在这里插入图片描述

接下来,用这7个参数的构造器来创建线程池的对象。代码如下

ExecutorService pool = new ThreadPoolExecutor(
    3,	//核心线程数有3个
    5,  //最大线程数有5个。   临时线程数=最大线程数-核心线程数=5-3=2
    8,	//临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
    TimeUnit.SECONDS,//时间单位(秒)
    new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
    Executors.defaultThreadFactory(), //用于创建线程的工厂对象
    new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);

括号内有7个参数

  • 参数一:corePoolSize : 指定线程池的核心线程的数量。

    • 正式合同工
  • 参数二:maximumPoolSize:指定线程池的最大线程数量。,除开核心线程的都是临时线程

    • 正式工+临时工=总人数
  • 参数三:keepAliveTime :指定临时线程的存活时间

    • 临时工聘用时间
  • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)

    • 如用TimeUnit.SECONDS表示秒
  • 参数五:workQueue:指定线程池的任务队列。缓存多个任务对象

    • 是除开被正式工接待外的正在排队的客户,当这个队列没满的时候不会创建新临时线程
    • new LinkedBlockingQueue(可以加无穷尽个客户)/new ArrayBlockingQueue(最多多少个客户)
  • 参数六:threadFactory:指定线程池的线程工厂。,为线程池创建(核心、临时)线程

    • hr,用于招人
    • 是接口,用Executors.defaultThreadFactory()来创建线程
  • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

    • new ThreadPoolExecutor.AbortPolicy()

    • 新任务拒绝策略

      策略详解
      ThreadPoolExecutor.AbortPolicy丢弃任务并抛出RejectedExecutionException异常。是默认的策略
      ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常 这是不推荐的做法
      ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待最久的任务 然后把当前任务加入队列中
      ThreadPoolExecutor.CallerRunsPolicy由主线程main负责调用任务的run()方法从而绕过线程池直接执行。也就是既不抛弃任务也不抛出异常,而是将某些任务回退到调用者,让调用者去执行它。

线程池工作原理

在这里插入图片描述

客户任务的执行顺序是:

  1. 3个任务先交给3个正式工
  2. 正式工满了存到任务队列
  3. 8个任务队列满了交给临时工
  4. 2个临时工临时工达到上限了就调用任务拒绝策略
  5. 上述表明超出3+8+2个任务的时候会调用任务拒绝策略

线程池处理任务方法

使用ExecutorService的常用方法

方法名称说明
void execute(Runnable command)执行 Runnable 任务
Future submit(Callable task)执行 Callable 任务,返回未来任务对象,用于获取线程返回的结
void shutdown()只是将线程池的状态设置为 SHUTDOWN 状态,等全部任务执行完毕后,再关闭线程池
List shutdownNow()首先将线程池的状态设置为 STOP,立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务(不推荐),并且抛出异常

注意事项

  • 临时线程什么时候创建?

    • **新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,**此时才会创建临时线程。
  • 什么时候会开始拒绝新任务?

    • **核心线程和临时线程都在忙,任务队列也满了,**新的任务过来的时候才会开始拒绝任务。
  • 线程池默认不会死亡,除非用shutdown、shutdownNow如

  • 核心线程数量到底配置多少呢??

    • 计算密集型的任务:核心线程数量 = CPU的核数 + 1
    • IO密集型的任务:核心线程数量 = CPU核数 * 2

File、IO、字符集

  • ASCII字符集:美国信息交换标准代码,包括了英文、符号等,只有英文、数字、符号等,占1个字节
  • GBK字符集:GBK兼容了ASCII字符集。汉字编码字符集,含了2万多个汉字等字符。汉字占2个字节,英文、数字占1个字节
  • Unicode:是国际组织制定的,可以容纳世界上所有文字、符号的字符集。
  • UTF-8字符集:是Unicode字符集的一种编码方案,汉字占3个字节,英文、数字占1个字节

字符编码和解码时使用的字符集必须一致,否则乱码。英文,数字一般不会乱码,因为很多字符集都兼容了ASCII编码。

File类只能对文件本身进行操作,不能读写文件里面存储的数据。File对象既可以代表文件、也可以代表文件夹。

分隔符正常是用/,也可以用\\(为什么是双反斜杠呢,因为单反斜杠加一个字母n可以表示换行\n),也可以用File.separator、+来拼接字符串

// File f1 = new File("D:/resource/ab.txt");
// File f1 = new File("D:\\resource\\ab.txt");
File f1 = new File("D:" + File.separator +"resource" + File.separator + "ab.txt");

相对路径默认是在当前模块而不是工程下面寻找文件,起始地址要为当前模块名而非工程名

// 绝对路径:带盘符的
        // File f4 = new File("D:\\code\\javasepromax\\file-io-app\\src\\itheima.txt");
        // 相对路径(重点):不带盘符,默认是直接去工程下寻找文件的。
        File f4 = new File("file-io-app\\src\\itheima.txt");

IO

IO流就是一个管道,是连接内存与本地数据/网络数据的桥梁

主要分为字节、字符输入/输出流四种,其中字节流适合所有类型的文件、做数据的转移如复制,字符流适合操作纯文本文件、读写文本内容。字节流并非只能转移只含ASCII字节的文件,转移图片文件也可以。字符流会根据字符编码将字符转换为字节。

在这里插入图片描述

上述除了原始流,下面的都是包装流,不能单独使用,需要结合原始流使用。

  • 缓冲流:对原始流进行包装,提高原始流读写数据的性能。先把数据读/写到缓冲流内部的8BK的数组中(ps: 先攒一车货),等数组存满了,再通过原始流,一次性读入内存/写到目标文件中去(把囤好的货,一次性运走)。

  • 打印流(区别于原始流):打印流自动处理字符编码,适合用于文本数据,可以打印输出任意类型的数据。

  • 数据流:把数据和数据的类型一并写到文件中去,读取的时候也将数据和数据类型一并读出来。

  • 序列化流:以对象为单位来读写数据。也就是把对象当做一个整体,可以写一个对象到文件,也可以从文件中把对象读取出来。

FileWriter写完数据之后,必须刷新或者关闭,写出去的数据才能生效。

反射(了解)

在 Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为 Java 语言的反射机制。

获取 Class 对象的 3 种方法 :

  • 调用某个对象的 getClass()方法

    Person p=new Person();

    Class clazz=p.getClass();

  • 调用某个类的 class 属性来获取该类对应的 Class 对象

    Class clazz=Person.class;

  • 使用 Class 类中的 forName()静态方法(最安全/性能最好)

    Class clazz=Class.forName(“类的全路径”); (最常用)

jdk1.8 的新特性

1 lambda表达式

用于简化匿名内部类,只能简化函数式接口的匿名内部类!!!

Lambda表达式的省略写法(进一步简化Lambda表达式的写法)

  • 参数类型可以省略不写。
  • 如果只有一个参数,参数类型可以省略,同时()也可以省略。
  • 如果Lambda表达式中的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号!此时,如果这行代码是return语句,也必须去掉return不写。

格式:

1.Lambda的标准格式
	(参数类型1 参数名1, 参数类型2 参数名2)->{
		...方法体的代码...
		return 返回值;
	}

2.在标准格式的基础上()中的参数类型可以直接省略
	(参数名1, 参数名2)->{
		...方法体的代码...
		return 返回值;
	}

//进一步省略写法

// 若只有一个参数, 省去()
 //若方法体只有一行代码,省去{}、;、return
3.如果{}总的语句只有一条语句,则{}可以省略、return关键字、“;”都可以省略
	(参数名1, 参数名2)-> 结果
	
4.如果()里面只有一个参数,则()可以省略
	参数名 -> 结果
    
    Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o2.getScore() - o1.getScore();
            }
        });
省略为
Arrays.sort(students,(o1,o2) -> o2.getScore() - o1.getScore());

2 方法引用

方法引用允许直接引用已有 Java 类或对象的方法或构造方法。

3 函数式接口

有且仅有一个抽象方法的接口叫做函数式接口,函数式接口可以被隐式转换为Lambda 表达式。通常函数式接口上会添加@FunctionalInterface 注解。

4 接口允许定义默认方法和静态方法

从 JDK8 开始,允许接口中存在一个或多个默认非抽象方法和静态方法。

5 Stream API

新添加的 Stream API(java.util.stream)把真正的函数式编程风格引入到 Java中。这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。可以用于操作集合或者数组的数据。

优势: Stream流大量的结合了Lambda的语法风格来编程,提供了一种更加强大,更加简单的方式操作集合或者数组中的数据,代码更简洁,可读性更好。

Stream流的使用步骤

在这里插入图片描述

6 日期/时间类改进

之前的 JDK 自带的日期处理类非常不方便,我们处理的时候经常是使用的第三方工具包,比如 commons-lang包等。不过 JDK8 出现之后这个改观了很多,比如日期时间的创建、比较、调整、格式化、时间间隔等。

这些类都在 java.time 包下,LocalDate/LocalTime/LocalDateTime。

7 Optional 类

Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent()方法会返回 true,调用 get()方法会返回该对象。

8 Java8 Base64 实现

Java 8 内置了 Base64 编码的编码器和解码器。

Java 的异常

简述throw和throws的区别?

  • throws在方法声明上,用于对此方法内出现的异常进行抛出,让上一层代码来解决
  • throw用于方法代码中,用于抛出指定异常对象

在这里插入图片描述

Throwable 是所有 Java 程序中错误处理的父类,有两种子类:Error 和Exception。

Error:表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的严重错误,导致 JVM 无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。

Exception:表示可恢复的例外,这是可捕捉到的。

1.运行时异常:都是 RuntimeException 类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java 编译器不会检查它(不try也行)。

2.非运行时异常(编译异常):是 RuntimeException 以外的异常,类型上都属于 Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、SQLException 等以及用户自定义的 Exception 异常。

常见的 RunTime 异常几种如下

空指针引用异常、下标越界异常

ClassCastException - 类型强制转换异常。

IllegalArgumentException - 传递非法参数异常。

ArithmeticException - 算术运算异常

ArrayStoreException - 向数组中存放与声明类型不兼容对象异常

NegativeArraySizeException - 创建一个大小为负数的数组错误异常

NumberFormatException - 数字格式异常

SecurityException - 安全异常

UnsupportedOperationException - 不支持的操作异常

同步锁、死锁、乐观锁、悲观锁

同步锁:

当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。

死锁:

何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

乐观锁:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_conditio 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

悲观锁:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中synchronized 和ReentrantLock 等独占锁就是悲观锁思想的实现。

高薪常问

Threadloal 的原理

ThreadLocal 并不是一个Thread,而是Thread的局部变量。

ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

客户端发送的每次请求,后端的Tomcat服务器都会分配一个单独的线程来处理请求。同一个线程上可以执行很多代码,如一个新增员工请求,会先被拦截器拦截校验,然后再执行接口代码…,通过ThreadLocal的这份存储空间,可以在上述这些步骤中实现数据存取,在线程生命周期内都能共享这份存储空间。

其实在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap(其类似于 Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap 中元素的 key 为当前ThreadLocal 对象,而 value 对应线程的变量副本。

ThreadLocal 本身并不存储值,它只是作为一个 key 保存到 ThreadLocalMap中,但是这里要注意的是它作为一个 key 用的是弱引用,因为没有强引用链,弱引用在 GC的时候可能会被回收。这样就会在 ThreadLocalMap 中存在一些 key 为 null 的键值对(Entry)。因为 key 变成 null 了,我们是没法访问这些 Entry 的,但是这些 Entry 本身是不会被清除的。如果没有手动删除对应 key 就会导致这块内存即不会回收也无法访问,也就是内存泄漏。

使用完 ThreadLocal 之后,记得调用 remove 方法。 在不使用线程池的前提下,即使不调用 remove 方法,线程的"变量副本"也会被 gc 回收,即不会造成内存泄漏的情况。

ThreadLocal常用方法:

  • public void set(T value) 设置当前线程的线程局部变量的值
  • public T get() 返回当前线程所对应的线程局部变量的值
  • public void remove() 移除当前线程的线程局部变量

synchronized 和 volatile 的区别是什么?

volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。

volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。

volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

synchronized 底层实现原理?

synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的 class 对象
  • 同步方法块,锁是括号里面的对象

算法手写

手写冒泡排序

在这里插入图片描述

递归

递归算法三要素:(以阶乘举例)

  1. 递归的公式: f(n) = f(n-1) * n;
  2. 递归的终结点:f(1)
  3. 递归的方向必须走向终结点:f(5) = f(4) * 5f(4) = f(3) * 4f(3) = f(2) * 3f(2) = f(1) * 2f(1) = 1

快排

快速排序是一种基于分治思想的排序算法。它的基本思想是选择一个基准元素,将数组分成两个子数组,一部分小于等于基准元素,一部分大于等于基准元素,然后对这两个子数组分别递归地进行排序。通常选择表中的第一个元素,用于划分顺序表为两部分,左边部分值小于枢轴,右边部分值大于枢轴。

算法步骤:

1.确定枢轴pivot,并用一个新变量存储:int pivot = A[low]
2.high: 依次从后往前扫描,如果 元素值 A[high] < pivot,则 A[low] = A[high],否则high–
3.low: 依次从前往后扫描,如果 元素值 A[low] > pivot,则 A[high] = A[low],否则low++

package chapter2.test22;
import java.util.Arrays;
/**
 * 快速排序(Quick Sort)是一种基于分治思想的排序算法。它的基本思想是选择一个基准元素,将
 * 数组分成两个子数组,一部分小于等于基准元素,一部分大于等于基准元素,然后对这两个子数组分别递归地进行排序。
 * 请在网上了解一下快速排序,编写一个程序实现该功能
 */
public class test22_3 {
    public static void main(String[] args) {
        int[] arr = {3,429,5,87,9,42};
        quickSort(arr, 0, arr.length - 1); // 对数组进行排序
        System.out.println(Arrays.toString(arr)); // 输出排序后的数组
    }
        public static void quickSort(int[] arr, int low, int high) {
        //输入左右位置合法
            if (low < high) {
                int pivot = partition(arr, low, high); // 获取基准元素位置
                quickSort(arr, low, pivot - 1); // 对左子数组进行递归排序
                quickSort(arr, pivot + 1, high); // 对右子数组进行递归排序
            }
        }
    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[low]; // 以最后一个元素为基准元素
        while (low<high) {
            while (low < high && arr[high] >= pivot) {
                high--;
            }
            arr[low] = arr[high];//比枢轴元素小的元素移动到左端
            while (low < high && arr[low] <= pivot) {
                low++;
            }
            arr[high] = arr[low];// 比枢轴元素大的元素移动到右端
        }
        arr[low] = pivot;// 枢轴元素放到最终位置
        return low;
        }
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值