Java基础

Java基础

面向对象

四大特性

抽象

将一些实体的共同特质抽取出来,放进一个概念中(类)

抽象是简化复杂现实世界问题的过程,通过创建模型来表示关键特征和行为。在面向对象编程中,抽象通常是通过类的形式实现的,类定义了一组相关的属性和方法,但不包含具体的实现细节。抽象使得程序员可以专注于问题的概念,而不是具体的实现细节,从而提高了代码的可读性和可维护性。

具体实现:类和接口

封装

相当于一个黑盒,我们只需要知道他能干什么,而不需要知道他要去做什么

封装是将对象的状态(属性)和行为(方法)结合在一起,并对外隐藏其内部实现细节的过程。这意味着对象的内部结构对外部是不可见的,只能通过对象提供的公共接口(方法)来访问和操作对象的状态。封装有助于减少系统的复杂性,并提高安全性和可维护性。

具体实现:private 方法

继承

子类继承父类的一些特性,可以减小代码的冗余度,提高代码重用性

继承是一种创建新类(子类)的方式,新类继承现有类(父类)的属性和方法。继承支持代码的重用,并允许新类扩展或修改父类的行为。通过继承,可以建立类之间的层次关系,使得子类具有父类的所有特性,同时还可以添加或覆盖父类的特性。

具体实现:类继承和接口实现

多态

子类继承父类,但是也可以有子类自己的行为

多态是指允许使用子类的对象来替代父类的对象。这意味着可以用子类特有的方法来覆盖父类的方法,当通过父类引用调用方法时,实际执行的是子类的版本。多态性提高了程序的灵活性和可扩展性,允许在运行时动态决定对象的实际类型。

具体实现: 可以实现多个接口
重载
重写

数据类型

基本数据类型

整型

  • byte:8位有符号的整数,范围从-128到127
  • short:16位有符号整数,范围从-32768到32767
  • int:32位有符号整数,范围从-2,147,483,648 到 2,147,483,647
  • long:64位有符号整数,范围从 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807

浮点型

  • float:32位单精度浮点数
  • double:64位双精度浮点数

字符型

  • char:16位Unicode字符,足以表示任何标准ASCII字符

布尔型

  • boolean:表示逻辑值true或false

引用数据类型

  • 表示创建对象的模板,对象是类的实例

接口

  • 定义了一组方法,但不提供实现。类可以实现一个或多个接口

数组

  • 表示相同类型元素的集合,可以是基本数据类型或对象类型的数组

大数类型

特点

  • 构造函数可以传入int、long、String
  • 使用构造new BigDecimal(0.1)值不为0.1,而是0.1000000000000000055511151231257827021181583404541015625
  • 如果使用String构造 new BigDecimal(“0.1”) 就是真正的0.1
  • 如果要使用double 类型构造 BigDecimal 先用Double.toString(double),再把String传入BigDecimal的构造

时间类型

Java的四种时间类型

  • java.util.Date
  • java.sql.Date
  • java.sql.time
  • java.sql.TimeStamp

对应关系

  • date:对应Java的java.sql.Date类型
  • TIME:对应Java的java.sql.Time类型
  • DATETIME、TIMESTAMP:对应Java的java.sql.TimeStamp类型

关键字

final

修饰类

  • 该类不可以被继承
  • 被final修饰的类不能被CGlib动态代理

修饰方法

  • 不可以被重写

修饰变量

  • 修饰基本类型 值不可以被改变
  • 修饰引用类型 引用类型的地址不可变,但是引用类型内的值可以变

transient

  • 修饰后不会被序列化

static

修饰类

  • 修饰后为静态内部类

修饰变量

  • 类变量(不同于实例变量 类变量在类加载的时候就被创建 存储在Java方法区)

修饰方法

  • 类方法
  • 没有this

静态代码块

  • 用来初始化数据

特点

  • 在类加载的第三阶段初始化阶段,使用方法进行初始化(是编译器给的方法)
  • static修饰的为类的成员,可以直接使用类名调用,不需要实例对象
  • 静态内部类只会在用到其时才会加载,且只加载一次,可以实现单例模式

接口

不同版本接口加入了不同功能

JDK1.7

  • 有常量、抽象方法
  • 常量必须使用public static final修饰,可以省略
  • 抽象方法就是abstract方法,abstract关键字可以不写

JDK1.8

  • 有默认方法
    • 用default标识
    • 默认方法可以不重写
  • 静态方法
    • 用static标识

JDK1.9

  • 私有方法

为什么要有接口?

解决Java只能单继承的缺陷

为什么JDK1.8引入了默认方法?

增加了可扩展性

为什么JDK1.9引入了私有方法?

为了提高代码重用消除冗余代码

特点

  • 接口有抽象方法
    • public abstract默认就存在可以省略
    • 抽象方法实现类必须实现

接口与类的区别

  • 接口没有构造方法;所以不能new接口
  • 接口没有静态代码块
  • 一个类可以实现多个接口

抽象类

  • 可以实现构造方法
  • 一定有抽象方法 (抽象方法不能有实现体)
  • 如果子类没有重写所有的构造方法,子类还是一个抽象类
  • 抽象类与接口的区别
    1. 构造方法:接口无、抽象类有
    2. 实现方法:接口默认方法可以不实现、抽象类不实现任意一个构造方法还是一个抽象类
    3. 实现类:一个类只能实现一个抽象类,但是可以实现多个接口

静态内部类

嵌套类,属于类的一部分

内部类

绑定在了this即实例上面,属于实例的一部分

静态内部类与内部类的区别

  • 静态内部类属于类;而内部类属于实例
  • 静态内部类在没有使用到的时候,不会加载;内部类只要外部类加载就会加载
  • 内部类可以访问外部类的一切方法与变量;静态内部类只能访问外部类的静态变量
  • 内部类不能有静态方法;静态内部类可以既有普通方法,又有静态方法;

异常

Throwable接口

Java的异常类都继承自java.lang.throwable类

Throwable有两个主要的子类Exception和Error

Error

运行中难以预料的异常

例如

  • OOM
    “OutOfMemory”通常指的是Java应用程序中的内存不足错误,导致无法分配更多的内存空间。这通常表现为java.lang.OutOfMemoryError异常
  • SOF
    SOF:表示栈溢出错误,表示堆栈已满,无法创建新的方法调用帧。 通常是调用递归层次过深。

Exception

可以预见的异常

分为两大类:检查型异常和非检查型异常

检查型异常(编译时异常)

  • 必须显示抛出或者捕获
  • 举例
    • IOException
    • InterruptedException

运行时异常

NullPointException

异常捕获

Try Catch Finally

  • 只要try的代码执行,finally的代码执行
  • 为什么finally必会执行?
    • try块有return,finally无return
      当执行到try块的return时,会将要return的值存入一个临时变量,然后去执行finally,最后返回临时变量的值
    • try块有return,finally有return
      这种情况下,finally的return会覆盖掉try的return的临时变量的值,所以返回finally的值
  • 如果finally有return,那就是返回这个值,如果没有,那么finally无论如何操作数据,都不会影响返回值

拆箱和装箱

什么是拆箱和装箱?

  • 装箱:将Java的基本数据类型(如intdouble等)转换为它们对应的包装类对象(如IntegerDouble等)的过程
  • 拆箱:将包装类对象转换回它们对应的基本数据类型的过程

为什么要装箱和拆箱?

  • 装箱和拆箱使得我们可以在需要对象类型的地方使用基本数据类型,这在集合框架中尤其有用,因为集合只能存储对象
  • 自动装箱和拆箱简化了代码,使得我们可以在不显式进行类型转换的情况下在基本类型和包装类之间自由切换

哪些基本数据类型有对应的包装类?

  • Java为每个基本数据类型提供了对应的包装类:IntegerDoubleFloatLongShortByteCharacterBoolean

ThreadLocal

是什么?用途?

ThreadLocal类提供了线程局部变量的实现。它允许每个线程都可以拥有自己的变量副本,这些副本对于其他线程是不可见的。这在处理线程安全问题时非常有用,尤其是当你想要避免同步或者使用全局变量时

如何解决多线程共享资源问题?

ThreadLocal通过为每个线程提供独立的变量副本来避免共享资源问题。这样,每个线程都可以独立地操作自己的资源,而不需要担心其他线程的干扰

工作原理?

ThreadLocal内部使用ThreadLocalMap来存储每个线程的局部变量。当一个线程首次访问ThreadLocal变量时,它会在当前线程的Thread对象中创建一个条目,并存储变量的值。之后,同一个线程的所有访问都会返回这个线程特有的值

同步机制相比有什么优势?

与同步机制相比,ThreadLocal避免了线程之间的竞争,因为它为每个线程提供了独立的变量副本。这可以减少锁的使用,提高程序的性能。此外,ThreadLocal也简化了代码,因为它不需要复杂的同步逻辑

常见的使用场景?

  • 为每个线程提供单独的数据库连接
  • 存储用户请求相关的数据,如会话信息
  • 缓存特定于线程的数据
  • 避免在多线程环境中传递线程不安全的实例

内存泄露问题?

ThreadLocal可能导致内存泄漏,因为ThreadLocalMap中的条目不会自动清理。如果线程的生命周期很长,或者线程对象被长时间保留,那么存储在ThreadLocal中的大对象可能会占用大量内存。当线程结束时,应该调用ThreadLocalremove方法来显式清理这些条目。

如何解决内存泄漏?

  • 在使用ThreadLocal的代码块结束后,显式调用ThreadLocalremove方法来清理存储的数据。
  • 避免在ThreadLocal中存储大对象或长时间存活的对象。
  • 使用WeakReference来引用ThreadLocal变量,以便在垃圾回收时能够清除这些引用。

ThreadLocalAtomicInteger/ConcurrentHashMap等并发工具类的区别?

ThreadLocal提供了线程隔离的变量,而AtomicIntegerConcurrentHashMap等并发工具类提供了线程安全的共享变量。ThreadLocal适用于不需要跨线程共享数据的场景,而AtomicIntegerConcurrentHashMap适用于需要在多个线程之间同步访问共享资源的场景

String相关

String

特点

  • final修饰,不可继承的类
  • 底层使用char[]数组存,也有final修饰,值不可变(JDK1.8)
  • JDK1.9使用byte[]数组存放
  • 为什么1.9要使用byte数组?
    为了节省内存,据调查存储的字符串70%都是拉丁字母,byte占一个字节,char占两个字节,占1个字节更省内存

字符串常量池

  • 本质 是一个固定大小的HashMap
  • 字面量会直接进入字符串常量池
  • String对象调用intern方法也会进入字符串常量池
  • 存放的位置 JDK1.6之前存放在方法区;JDK1.7之后存放在堆区

intern()方法

  • JDK1.6中

    如果池中没有,那么就再创建一个新的对象放在池中

  • JDK1.7后

    如果池中没有,就会将当前这个对象的地址复制一份,放入堆中,不用重新创建对象了

new String(“a”)会创建几个对象?

  • 2个
    • new String一个
    • 字面量a一个

String b = new String(“a”) + “B”;会创建几个对象?

  • 4个对象
    • newString一个
    • a
    • b
    • 拼接用到了StringBuilder,也是一个

StringBuilder

  • 线程不安全
  • 但是会更快一点
  • String之间的拼接默认就是用StringBuilder优化的

StringBuffer

  • 方法中有synchronized进程同步关键字 线程安全但是效率慢
  • StringBuilder 和 StringBuffer 继承自AbstractStringBuilder类

IO

BIO

阻塞IO 即IO会阻塞当前线程,必须等待IO完成后才能继续执行

Socket

TCP通信
  • Socket
    • 客户端Socket
    • API
      • connect(new InetSocketAddress(“IP”, port)) 连接客户端
      • socket.getOutputStream() 获取输出流,向服务器write数据
  • ServerSocket
    • 服务端Socket
    • API
      • bind 监听一个本地的端口号
      • accept 阻塞等待客户端连接,所谓的BIO在于此
      • getInputStream 获取输入流,读入客户端数据
UDP通信
  • DatagramSocket
    • 客户端服务器端都使用这个类
    • API
      • new DatagramSocket (port) 绑定一个端口创建服务
      • send(DatagramPacket) 发送数据报
  • DatagramPacket
    • UDP无需建立连接,每一个数据报内部传输IP+port
    • API
      • atagramPacket(byte[] buf, int length, InetAddress addr, int port)

弊端

一个Socket需要一个线程,浪费性能

NIO

非阻塞IO或者叫New IO

IO操作是非阻塞的,即IO操作不会阻塞当前线程,需要主线程一段时间来判断一次是否IO完毕

NIO与BIO的区别

  • BIO面向流;NIO面向缓冲区
  • BIO单向,要么读要么写;NIO双向
  • BIO 是阻塞的;NIO 是非阻塞的
  • BIO只能向后读;NIO可以前后读
    BIO 读写是面向流的,一次性只能从流中读取一个或者多个字节,并且读完之后流无法再读取,除非我们缓存起来

Java具体主要使用三个组件来实现NIO

image-20240326211804092

Channel
分类
  • 继承了SelectableChannel的类
    • SocketChannel TCP客户端
    • ServerSocketChannel TCP服务器端
    • DatagramChannel UDP数据报
  • FileChannel 不能被复用
特点
  • 操作是双向的
  • 异步
  • 不能直接访问数据,需要与buffer配合使用
Selector
  • 每一个Channel都得在Selector上注册,注册后会返回一个选择键SelectionKey
    selectionKey
    代表Selector与Channel关系的类

    可以通过其获得Selector与Channel

  • 每执行一次select()方法,都会返回一个当前就绪的通道的数量

  • 维护三个集合

    • keys 已注册键的集合
    • selectKeys 已选择的键的集合(即就绪的键的集合)
    • 已取消的集合
  • 建立Selector系统

    • Selector.open()创建一个Selector
    • 设置通道为非阻塞
    • 通道调用register()注册在此Selector上
      • 关心的操作有
        1. Read
        2. Write
        3. Connect
        4. Accept
      • 指向过程
        1. 检查已取消的键的集合
        2. 检查已注册的键的集合
        3. 返回值
Buffer

通道的通信都需要经过Buffer来实现

创建
  • allocate(long) 传入一个大小,开辟指定大小的缓存,缓存开辟在JVM的堆
  • allocateDirect(long) 分配一个直接缓存区,开辟在JVM之外
  • wrap(byte[]) 将一个byte数组作为一个缓存
直接缓冲区和非缓冲区的区别
  • 直接缓冲区的开销少,少一次复制的过程
  • 假设给一个通道传入了一个非直接缓冲区,那么通道会先创建一个临时的直接缓冲区,将非直接缓冲区的数据复制到临时的直接缓冲区,使用这个临时的直接缓冲区去执行 IO 操作(多一次拷贝,增大了开销)
API
  • flip
    将写模式转换为读模式,将当前的limit设置为position,然后将position设为0

  • compact

    压缩,将未读取的数据(position与limit之间的数据)向前移动

  • hasRemaining
    读取时在while循环内使用hasRemaining判断,判断position与limit之间的距离

维护了四个值
  • mark 标记位置,每次reset会回到这个位置
  • position 当前位置,每写入一个就+1(指向最新元素的下一个空白的位置)
  • limit 第一个不能被读写的位置
  • capacity 总容量

Collection

子接口

list

ArrayList

用数组实现的列表,适合查询,不适合增删

结构特点
  • 由一个对象数组构成
  • 初始容量10,最大容量Integer.max - 8 | 为什么减去8?
    • 给虚拟机预留一些数据存放对象的其他数据
  • grow() 扩容
    • 扩容每次扩容1.5倍,扩容方式直接调用System.copyOf()
    • ArrayList有缩容方法trimSize,但是不会去自动调用,需要执行
  • clone() 浅拷贝
    就是创建一个对象的副本,这个对象的引用还是指向原来对象的,即新对象和原来对象共用同一个内存空间
  • get(index)方法
    检查是否越界,直接返回对应下标数组的数据
  • set(index, value)方法 返回旧数据
  • add(value) 直接添加到末尾
  • add(index, value)
    调用System.copy将index之后的所有数据,向后copy一格,然后把这个数据放在这里
  • remove(index)
    • 直接调System.copy将数据从index后覆盖前一个数据即可
    • remove操作会更改modcount值
    • modCount用于Fast-Fail检测
      • 在使用迭代器遍历集合时,如果在遍历过程中有其他线程修改了集合的内容,迭代器会在下一个 next() 调用时检测到 modCount 的变化,并抛出 ConcurrentModificationException,以避免产生不一致的结果
      • 如果是fori遍历,然后remove,不会报这个错
      • 如果是迭代器或是foreach遍历,然后remove,就会报这个错
  • indexOf(value)
    返回该值第一次出现的下标
  • lastIndexof(value)
    返回该值最后一次出现的下标
LinkedList
  • 用链表实现的列表
  • 适合增删,不适合查询
  • 是一个双向队列
Vector
子类 Stack

用vector实现的栈,已经退出Java舞台,推荐使用Deque来代替这个类

是一个线程安全类 方法上有synchronized同步关键字,实现了线程安全

结构特点
  • 默认大小10
  • 扩容倍数,可以自己设置,如果不设置默认成倍增长

set

HashSet
  • 子类
    LinkedHashSet

  • 底层由一个HashMap实现

  • 可以存null

  • 如何实现了去重?

    • HashSet的内部结构是一个HashMap。当你向HashSet添加元素时,实际上是将元素作为键(key)放入这个内部的HashMap中。HashMap使用元素的hashCode()方法来计算哈希值,并根据这个哈希值将元素存储在不同的桶(bucket)中
    • 每个对象都有一个hashCode方法,它返回一个整数值,这个值是由对象的内容计算得到的。如果两个对象的hashCode相同,它们可能会存储在同一个桶中,但这并不意味着它们是相同的对象
    • HashSet尝试添加一个新元素时,它会首先调用这个元素的hashCode()方法来获取哈希值,然后根据哈希值找到对应的桶。在同一个桶中的元素,HashSet会通过调用equals()方法来检查是否已经存在一个相同的对象。如果equals()返回true,则认为这两个对象相同,新元素不会被添加到HashSet中,从而实现了去重
    • 由于HashSet内部使用HashMap,它能够提供快速的查找和插入操作。当查找一个元素是否存在时,HashSet会使用元素的hashCode()来快速定位到桶,然后在桶中通过equals()方法来检查元素是否存在

queue

PriorityQueue
  • 底层是一个二叉堆 存储用一个Object数组来存储
  • 实现了Comparator接口,通过重写compare()方法来实现,来进行比较
  • 默认排序为从小到大
Deque
  • LinkedList
  • ArrayList

核心API

  • size()
  • contains()
  • iterator()
  • toArray()
  • add()
  • remove()
  • containsAll()
  • addAll()

迭代器

Collection继承了Interator,重写iterator方法,可以返回一个迭代器

核心API

  • hasNext()
  • next()
  • remove()
  • 迭代方式 使用while循环,判断条件hasNext

Fast-Fail 快速失败机制

集合类都有快速失败机制,迭代遍历过程中如果对集合类内容进行了更改,就会抛出异常

  • 原理:通过内部定义的一个字段 modCount来判断是否更改过这个集合,如果修改了集合(add或者remove),那么modCount也会被修改,检测到modCount值不同后,就抛出一个ConcurrentModificationException

安全失败机制

  • 对于JUC包下的类比如CopyOnWriteArrayList,就不会抛出ConcurrentModificationException异常
  • 因为迭代器修改的是原容器的复制,而不是容器本身,这样的机制称为安全失败机制

for each

  • Java的一个语法糖,其实内部就是使用迭代器迭代的

  • for each操作不能增删元素,会抛出异常

迭代进行删除操作

  • 可以使用for i
  • 可以使用迭代器的remove方法

Map

HashMap

结构特点

  • 初始容量大小

  • 默认负载因子0.75

    • 为什么负载因子是0.75?
      根据泊松分布,0.75可以达到一个不错的散列程度
    • 负载因子有什么作用?
      1. 负载因子控制着hash数组的散列程度
      2. 负载因子大,节省内存,但是会导致哈希碰撞比较严重
      3. 负载因子小,查询快,但是会很浪费内存
  • 链化与树化

    • 当链表长度大于8,且数组size达到64会将链表转化为红黑树
    • 当链表长度小于6 会将红黑树转化为链表
    • 为什么变化阈值为6和8? 为了防止复杂度震荡
  • 扩容大小一定是2的倍数

    方便查找元素 根据keyHash & (size - 1)就可以快速找到下标元素

  • 为什么要重写equals和hashCode方法

    当你重写了equals方法来定义对象的相等性时,也应该重写hashCode方法,以确保hashCodeequals保持一致。如果两个对象根据equals方法比较是相等的,那么它们应该返回相同的hashCode

  • 遍历

    可以foreach+keySet方法遍历

    可以用foreach+entrySet方法遍历

  • HashMap不是线程安全类,不要在并发下使用

  • resize方法

    • 承担两个任务:1 初始化Node数组 2 进行扩容操作
    • 每次扩容大小翻倍
    • JDK1.7扩容时头插法,JDK1.8扩容时尾插法
    • 新下标:hash & (newTable.length-1)
      其实就是:要不然就是旧下标,要不然就是旧下标加上旧数组的长度

JDK1.7与JDK1.8的变化

JDK1.7
  • 由数组+链表实现
  • hash计算直接使用hashcode
  • 头插法
    • 扩容时会改变链表的顺序,可能会形成链表环
    • 在并发情况下,如果此时正好进行了扩容与插入,容易形成链表环,cpu占用直接100%
  • 扩容在插入节点之前
  • 寻找节点下标 用indexFor()方法
  • 对于key为null有专门的操作putForNullKey
    这个方法就是找0的桶,看看有没有key为null的对象
JDK1.8
  • 由数组+链表+红黑树实现
    为了解决链表过长,引起遍历速度下降的问题
    也是为了解决链表环的问题
  • 寻找下标,直接用keyHash & (size - 1)即可,去除了indexFor方法
  • 扩容在插入结点之后
  • 尾插法
    扩容时不会改变链表的顺序
  • 哈希方法:前16位和后16位异或在运算
    将高位与低位混合,增大散列程度,使散列出来的数组更加的均匀
  • 对于key为null没有专门的操作

HashTable

  • 初始容量为11
  • 负载因子也是0.75
  • 线程安全,使用synchronized保证了线程安全

TreeMap

三个Map的区别

  • 数据结构组成

    • HashMap 数组加红黑树加链表
    • HashTable 数组加链表
    • TreeMap 数组加红黑树
  • 是否线程安全
    HashTable线程安全,其他两个不是

  • key是否可以为null

    HashMap可以,其他两个不可以

  • 是否有序

    TreeMap有序,其他两个无序

Object

所有类的超类

所有的方法都是native方法,是由C/C++实现的

核心API

registerNatives():该方法在static静态代码块调用,注册所有的native方法

通用

  • hashCode()
    • 获取哈希值
    • 哈希值的实现很复杂,其不仅仅是简单的返回一个地址值,分了很多种情况,具体不需要再做了解
  • equals()
    • 默认就是使用==判断
  • clone()
    • 配合Cloneable接口使用,用来对一个类进行一个Copy,默认为浅拷贝
  • toString()
    • 转化为字符串
    • 默认打印此Class对象的名称+@+哈希值
  • finalize()
    • 类似于C++析构函数,但有所区别,其不需要我们自己调用
    • 如果我们希望一个对象被GC时可以被复活,可以在这个方法内给该对象重新指向一个位置,使该对象重新可达
    • 会被一个低优先级的线程执行

反射

  • getClass() 获取类对象

多线程

  • notify()
    • 随机唤醒一个在等待队列中的线程
    • 让线程进入RUNNABLE状态
  • notifyAll()
    • 将等待队列中的线程全部唤醒
    • 让线程进入RUNNABLE状态
  • wait()
    • 进入等待,会释放锁资源
    • 让线程进入WAITING状态
  • wait(long)
    • 加了时间的等待状态
    • 让线程进入TIMED_WATING状态

四种引用

强引用

比如new的对象,只要强引用存在就不会回收,即使报OOM也不会回收

软引用

  • java.lang.ref.SoftReference类实现

  • 内存不足时会回收

  • 通常用于实现内存敏感的缓存

弱引用

  • 通过java.lang.ref.WeakReference类实现

  • 不会阻止垃圾回收器回收其指向的对象

  • 常用于监听对象的声明周期例如弱键WeakHashMap

虚引用

  • Java.lang.ref.PhantomReference类实现

  • 几乎总是处于可回收状态

  • 主要用于在对象被回收时接受一个系统通知或者执行一些清理操作

  • 不会访问对象,而是作为对象被回收的一个信号

弱/虚区别

  • 弱引用:允许在对象被回收前访问对象
  • 虚引用:接受对象被回收的通知