1 Java概述

标识符的命名规范

1.包名:
2.类名、接口名:XxxYyyZzz
3.变量名、方法名::xxxYyyZzz
4.常量名:XXX_YYY_ZZZ

java采用unicode字符集,utf-8是unicode的一种实现方式

变量必须先声明和初始化才能使用


2 数据类型与运算符

计算机底层存储的是整数的补码

补码:正整数的补码与原码一致,负整数的补码是在反码基础上加1

位运算符的运算过程都是基于补码运算,但是看结果,我们得换成原码,再换成十进制看结果

1.整数
  • Byte:适用于不超过 3 位整数范围的情况。

    • 实际范围:-128 到 127。

      注意128陷阱

  • Short:适用于不超过 5 位整数范围的情况。

    • 实际范围:-32,768 到 32,767。
  • Integer:适用于不超过 10 位整数范围的情况。

    • 实际范围:-2,147,483,648 到 2,147,483,647。
  • Long:适用于不超过 19 位整数范围的情况。

    • 实际范围:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。
    • 最多19位数字(例如,9,223,372,036,854,775,807)。
  • BigInteger:适用于超过 Long 类型范围的整数。

2.浮点数

有效数字是指从第一个非零数字开始到最后一个非零数字结束的所有数字。例如:
1234567.0 有7位有效数字:1, 2, 3, 4, 5, 6, 7。

  • Float:适用于表示 6 位有效数字的单精度浮点数,第7位不准确
  • Double:适用于表示约 15 位有效数字的双精度浮点数,第16位不准确
  • BigDecimal:适用于需要高精度浮点数运算的场景。

当byte、char、short三种类型的变量做运算时,结果为int型

short s1 = 10;
s1 += 2;//结论:不会改变变量本身的数据类型

如何区分&,|,^是逻辑运算符还是位运算符?

如果操作数是boolean类型,就是逻辑运算符,如果操作数是整数,那么就是位运算符。

逻辑运算符,是用来连接两个布尔类型结果的运算符(!除外


3 流程控制语句

如果if-else结构中的执行语句只有一行时,对应的一对{}可以省略的。

witch结构中的表达式,只能是如下的6种数据类型之一:byte 、short、char、int、枚举类型(JDK5.0新增)、String类型(JDK7.0新增)

case 之后只能声明常量。


4 数组

创建数组对象会在内存中开辟一整块连续的空间

char型默认值0或’\u0000’,而非’0’,为什么?

数组内存解析

image.png

第1次JVM内存解析

栈里面放地址,局部变量放栈里面

堆:开辟空间,放new出来的结构:放地址值(首地址值)、对象中的属性,

二维数组的内存解析

image.png


5 面向对象 一

类的实例化与对象的内存解析

第二次JVM内存解析:

无标题2

我们将局部变量存储在栈结构中

堆,我们将new出来的结构(比如:数组、对象)加载在堆空间中。

补充:对象的属性(非static的)加载在堆空间中。

static的呢?static的属性则加载在方法区

方法区:存放类的加载信息、常量池、静态域

JVM中的类的加载器和解释器对生成的字节码文件进行解释运行。

属性 vs 局部变量:

1. 局部变量:没默认初始化值。

2.在内存中加载的位置:

属性:加载到堆空间中(非static) 局部变量:加载到栈空间

return关键字后面不可以声明执行语句。

方法的内存解析

第3次JVM内存结构

无标题3

可变个数形参的说明

可变个数形参的方法与本类中方法名相同,形参类型也相同的数组不能共存。

可变个数形参在方法的形参中,必须声明在末尾

可变个数形参在方法的形参中,最多只能声明一个可变形参。

重写规则

子类和父类中的同名同参数的方法要么都声明为非static的(考虑重写),要么都声明为static的(不是重写而是隐藏了父类的方法)。

属性赋值顺序

总结:属性赋值的先后顺序
① 默认初始化
② 显式初始化
③ 构造器中初始化
④ 通过”对象.方法” 或 “对象.属性”的方式,赋值
以上操作的先后顺序:①- ②-③ -④

先显示初始化再构造器初始化

JavaBean:一个无参的公共构造器(反射造对象用)

成员内部类:

如何创建成员内部类的对象?(分静态的和非静态的)

​ 创建静态的Dog内部类的实例(静态的成员内部类):

这里假设DogPerson类的静态成员内部类。

Person.Dog dog = new Person.Dog();

​ 创建非静态的Bird内部类的实例(非静态的成员内部类):

1
Person.Bird bird = new Person.Bird();//错误的,为什么?

需要使用外部类的实例来调用内部类的构造函数:

在Java中,非静态内部类与外部类的实例是有关联的,因为非静态内部类隐含地包含了对外部类实例的引用。创建非静态的成员内部类的实例时,需要使用外部类的实例来调用内部类的构造函数,建立与外部类的实例相关联,所以应该这样调用:

1
2
Person p = new Person();
Person.Bird bird = p.new Bird();

外部类与局部类的方法调用

​ 外部类调内部类方法:

外部类.this.方法

​ 内部类调外部类的方法

在局部内部类的方法中(比如:show)如果调用外部类所声明的方法(比如:method)中的局部变量(比如:num)的话,要求此局部变量声明为final的。

原因:如果外部方法的局部变量可以被修改,那么在局部内部类的方法执行期间,外部方法的局部变量的值可能会发生改变。这会导致局部内部类访问到一个不一致的值,从而产生潜在的错误。

1
2
3
4
5
6
7
8
9
10
11
12
    public void method(){
//局部变量
int num = 10;

class AA{

public void show(){
// num = 20;
System.out.println(num);
}
}
}

jdk 7及之前版本:要求此局部变量显式的声明为final的
jdk 8及之后的版本:可以省略final的声明
总结:
成员内部类和局部内部类,在编译以后,都会生成字节码文件。
格式:

成员内部类:外部类$内部类名.class

局部内部类:外部类$数字 内部类名.class

6 面向对象 二

规定:”this(形参列表)”必须声明在当前构造器的首行

import static:导入指定类或接口中的静态结构:属性或方法。

如果在源文件中,使用了不同包下的同名的类,则必须至少一个类需要以全类名的方式示。

使用”xxx.xx”方式表明可以调用xxx包下的所结构。但是如果使用的是xxx子包下的结构,则仍需要显式导入(xx表示类或接口)。

我们在类的构造器中,针对于"this(形参列表)"或"super(形参列表)"只能二一,不能同时出现

由于类只会加载一次,则静态变量在内存中也只会存在一份:存在方法区的静态域中。

3.静态变量内存解析:

字符串常量池在jdk6及之前,存放在方法区。jdk7及之后,存放在堆空间`

​ static final 用来修饰属性:全局常量

虽然创建子类对象时,也调用了父类的构造器,但是自始至终就创建过一个对象,即为new的子类对象(没new不算创建对象)。

Object 类clone()的理解

clone()方法是浅拷贝

深拷贝实现方式:

  • 手动复制: 需要手动创建新对象,并对所有引用类型的字段进行递归复制。
  • 序列化/反序列化: 可以通过将对象序列化成字节流,然后再反序列化成新对象来实现深拷贝。

Object 类finalize()

finalize():当垃圾收集器准备回收一个对象时,它会首先调用该对象的 finalize() 方法。这个方法的默认实现是空的,但你可以在你自己的类中覆盖这个方法,以便在对象被销毁前执行一些必要的清理工作,比如关闭文件、释放资源等。


7 面向对象 三

体现三:单例模式(将构造器私有化)

static \ final \ abstract \native 可以用来修饰方法

当一个方法被声明为native 时,它的实现体并不在Java代码中,而是由本地的原生代码提供。这样的方法被称为本地方法(Native Method)。

多态性的使用:虚拟方法调用

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

1.关于向上转型与向下转型:

1 向上转型:多态(调用父类的方法)
2.向下转型(强转,调用子类的方法):

如何才能调用子类特的属性和方法?使用向下转型。

多态性的理解:

① 实现代码的通用性。

③ 抽象类、接口的使用肯定体现了多态性。(抽象类、接口不能实例化)

作用总结:由于虚拟方法调用,能增加程序拓展性。


8 面向对象高级

抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)`

全局常量:public static final的.但是书写时,可以省略不写
抽象方法:public abstract的,可以省略不写

JDK7及以前:只能定义全局常量和抽象方法

JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法(略)

接口与接口之间可以继承,而且可以多继承

知识点1:接口中定义的静态方法,只能通过接口来调用

知识点3:如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,那么子类在没重写此方法的情况下(不存在多态),默认调用的是父类中的同名同参数的方法。–>类优先原则

如果枚举类中只一个对象,则可以作为单例模式的实现方式。

注解用来配置应用程序的任何切面,简化代码和XML配置等。

框架 = 注解 + 反射机制 + 设计模式

JDK8中注解的新特性:可重复注解、类型注解

5.1 可重复注解:

能重复使用

5.2 类型注解:

9 异常处理

像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的释放。此时的资源释放,就需要声明在finally中。


10 多线程√

进程作为资源分配的单位

线程作为调度和执行的单位

image.png

每个线程,拥有自己独立的:栈、程序计数器
多个线程,共享同一个进程中的结构:方法区、堆。

yield():释放当前cpu的执行权

join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。

8 stop():已过时。当执行此方法时,强制结束当前线程。替换为interrupt()

1.常见线程优先级
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 –>默认优先级

2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
notify() / notifyAll()唤醒

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。

image.png

新建状态(new)、就绪状态(runnable)、运行状态(running)、阻塞状态(blocked)、死亡状态(dead)。
`阻塞又可以分为3种:

锁阻塞:对象锁被其他对象占用

无限等待:休眠状态,等待另一个线程执行唤醒操作

有限等待:有限时间的休眠状态

线程通信:wait() / notify() / notifyAll() :此三个方法定义在Object类中的。

notify() :notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个,线程优先级的作用就出来了。

在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。(这样创建的线程类,this是指向同一个对象,相同)

在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。(这样创建的线程类,this是指向各个新创建的对象,不同)

说明

2.1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
2.3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

3 sleep() 和 wait()的异同?
3.1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
3.2.不同点:
①两个方法声明的位置不同:Thread类中声明sleep() (静态的) , Object类中声明wait()
②调用的要求不同:

sleep()可以在任何需要的场景下调用。

wait()必须使用在同步代码块或同步方法中

③关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
关于同步方法的总结:
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身

操作同步代码时,相当于是一个单线程的过程,效率低。

多线程-线程安全的懒汉式死锁_ReentrantLock 的使用

Lock属于乐观锁,使用多个线程操作的是同一个变量
synchronized属于悲观锁

1.1面试题:synchronized 与 Lock的异同?

相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器 Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())

为什么这个顺序?lock更灵活,代码块范围更小,效率更高

Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —>同步方法(在方法体之外)

1.2.notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个,线程优先级的作用就出来了。

2.1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
2.3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

3.面试题:

面试题:sleep() 和 wait()的异同?
3.1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。有限等待和无限等待
3.2.不同点:
①两个方法声明的位置不同:Thread类中声明sleep() (静态的) , Object类中声明wait()
②调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
③关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

3. Lock和synchronized区别(悲观锁和乐观锁的区别)

a. Lock属于乐观锁, 使用多个线程操作的是同一个变量
synchronized属于悲观锁,使用多个线程操作一段代码
b.乐观锁:线程A在操作变量时,允许线程B操作,只是会先判断,如果有问题,就放弃本次操作.判断如果没有问题,就会正常操作
悲观锁:当线程A正在操作的时候,不允许线程B执行,要等A出来之后B才有可能进入执行

c.相对来说,悲观锁效率比较低,乐观锁效率比较高

image.png

image.png

新增方式一:实现Callable接口。 --- JDK 5.0新增

​ 1.1.创建一个实现Callable的实现类

1
class NumThread implements Callable{

​ 1.2.实现call方法,将此线程需要执行的操作声明在call()中

//这时没学泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	 @Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
​ System.out.println(i);
​ sum += i;
​ }
​ }
return sum;
​ }
}
public class ThreadNew {
public static void main(String[] args) {

​ 1.3.创建Callable接口实现类的对象

1
NumThread numThread = new NumThread();

​ 1.4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象

1
FutureTask futureTask = new FutureTask(numThread);

​ 1.5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()

1
2
    new Thread(futureTask).start();
try {

​ 1.6.获取Callable中call方法的返回值

//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。

1
2
3
4
5
6
7
8
9
         Object sum = futureTask.get();
​ System.out.println("总和为:" + sum);
​ } catch (InterruptedException e) {
​ e.printStackTrace();
​ } catch (ExecutionException e) {
​ e.printStackTrace();
​ }
​ }
}

说明:如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
①call()可以返回值的。
②call()可以抛出异常,被外面的操作捕获,获取异常的信息
③Callable是支持泛型的

2.新增方式二:使用线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}

public class ThreadPool {
public static void main(String[] args) {

1.提供指定线程数量的线程池

1
ExecutorService service = Executors.newFixedThreadPool(10);

2.设置线程池的属性

1
2
3
System.out.println(service.getClass());
((ThreadPoolExecutor) service).setCorePoolSize(15);
((ThreadPoolExecutor) service).setKeepAliveTime(3600, TimeUnit.SECONDS);

3.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象

​ service.execute(new NumberThread());//适合使用于Runnable
​ service.execute(new NumberThread1());//适合使用于Runnable

//service.submit(Callable callable);//适合使用于Callable
3.关闭连接池
service.shutdown();
}
}
好处:
①提高响应速度(减少了创建新线程的时间)
②降低资源消耗(重复利用线程池中线程,不需要每次都创建)
③便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止


11 常用类与基础API√

String内部定义了final char[] value用于存储字符串数据 (jdk9开始变byte[]了)

String s = new String(“abc”);方式创建对象,在内存中创建了几个对象?
两个:一个是堆空间中new结构,另一个是char[]对应的常量池中的数据:"abc"

当字符串是通过连接运算符连接时,会在堆内存中创建新的字符串对象,而当字符串是在编译期间已知的时候,会被优化成同一个对象。

5.1.3.intern()方法作用是将字符串对象放入字符串池中

数据类型的转换

474ed3e60f0e42688d230ca19713c6cd

4.常用类与基础 API-StringBuffer 与 StringBuilder 的源码分析、常用方法

两者基本一样

初始化时是16或者16+字符串长度

默认扩容为原来的2倍+2,并将原有的元素复制到新的数组中

日期时间格式化类:DateTimeFormatter

1
2
3
4
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); 
//格式化
String str4 = formatter3.format(LocalDateTime.now()); System.out.println(str4);
//2019-02-18 03:52:09 解析 TemporalAccessor accessor = formatter3.parse("2019-02-18 03:52:09"); System.out.println(accessor);

常用类与基础 API-使用 Comparable 接口实现自然排序

1.实现Comparable接口,重写compareTo(obj)方法

2.使用 Comparator 的对象,重写compare(Object o1,Object o2)方法,比较o1和o2的大小:


12 集合框架√

Arrays.asList(new int[]{123, 456})返回的是一个固定大小的列表,不能进行添加操作

5.概览

|----ArrayList、LinkedList、Vector

|—-HashSet、LinkedHashSet、TreeSet

|—-HashMap、LinkedHashMap、TreeMap,Hashtable,Properties

image.png

向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals().(因为collection中的相关方法contains()/remove()需要重写)

2.Collection接口常用方法:

Collection接口常用方法:

add(Object obj),
addAll(Collection coll),
size(),
isEmpty(),
clear();
contains(Object obj),
containsAll(Collection coll),
remove(Object obj),
removeAll(Collection coll),
retainsAll(Collection coll), //求两个集合的交集
equals(Object obj);
hasCode(),
toArray(),
iterator();

没有get()和set()方法

集合框架-List 接口常用方法的测试

增:add(Object obj)
删:remove(int index) / remove(Object obj)
改:set(int index, Object ele) 查:get(int index)
插:add(int index, Object ele)
长度:size()

Set接口中没额外定义新的方法,使用的都是Collection中声明过的方法。

遍历

遍历:

① Iterator迭代器方式

Collection集合的专用遍历方式`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {

//创建集合对象
Collection<String> c = new ArrayList<>();

//添加元素
c.add("hello");
c.add("world");
c.add("java");
c.add("javaee");

//Iterator<E> iterator():返回此集合中元素的迭代器,通过集合的iterator()方法得到
Iterator<String> it = c.iterator();

//用while循环改进元素的判断和获取
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
}

② 增强for循环

JDK5之后出现的,其内部原理是一个Iterator迭代器,实现Iterable接口的类才可以使用迭代器和增强for循环

注意:增强for循环内部不能直接操作集合的元素,只推荐做遍历使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
list.add("f");

//1,数据类型一定是集合或者数组中元素的类型
//2,str仅仅是一个变量名而已,在循环的过程中,依次表示集合或者数组中的每一个元素
//3,list就是要遍历的集合或者数组
for(String str : list){
System.out.println(str);
}
}

③ 普通的循环

set接口

Set接口中没额外定义新的方法,使用的都是Collection中声明过的方法。

集合框架-List 不同实现类的对比

集合框架-Set 不同实现类的对比及 Set 无序性、不可重复性的剖析`

以HashSet为例说明:

无序性:无序性不等于随机性。存储的数据在底层中不是依次排列的,表现为无序性`

2.元素添加过程:(以HashSet为例)

我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置)

判断数组此位置上是否已经有元素:
如果此位置上没其他元素,则元素a添加成功。 —>情况1
如果此位置上有其他元素b(或以链表形式存在的多个元素,则比较元素a与元素b的hash值:
如果hash值不相同,则元素a添加成功。—>情况2
如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,元素a添加失败
equals()返回false,则元素a添加成功。—>情况3

对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。

为啥不是覆盖?

避免覆盖错误: 如果直接覆盖,当两个键的哈希值相同但它们不是同一个键时,可能会导致数据丢失或错误。

jdk 7 :元素a放到数组中,指向原来的元素。 jdk 8 :原来的元素在数组中,指向元素a
总结:七上八下

3.常用实现类:

|—-Set接口
​ |—-HashSet:作为Set接口的主要实现类;底层用HashMap,即使用数组+单向链表+红黑树结构进行存储,
​ |—-LinkedHashSet:在HashSet现有基础上,又添加了一组双向链表,用于记录添加元素的先后顺序
|----TreeSet:底层用红黑树存储,可以按照添加的元素的指定的属性的大小顺序进行遍历

遇到线程安全问题用collections自己包一下

1
List<String> threadSafeList = Collections.synchronizedList(arrayList);

5.set中无序性,不可重复性的理解的要求:

5.1.无序性:!=随机性
添加元素的顺序和遍历元素的顺序不一致,是不是就是无序性呢?no!
什么是无序性?与添加的元素的位置有关,不像ArrayList一样是一次紧密排列的。
5.2.不可重复性:添加到Set的元素是不能相同的,比较的标准,需要判断hashCode()得到的哈希值、equals()得到的boolean型结果,哈希值相同且equals()返回true,则认为元素是相同的。

添加到hashset/linkedhashset中元素的要求:

要求元素所在的类要重写两个方法: equals()和hashCode()。
​ 同时,要求equals()和 hashCode()要保持一致性!我们只需要在IDEA中自动生成两个方法的重写即可

集合框架-TreeSet 的使用

1.向TreeSet中添加的元素的要求

要求是相同类型的对象,否则报异常。

7.集合框架-Map不同实现类的对比与 HashMap 中元素的特点

1.常用实现类结构

|—-Map:双列数据,存储key-value对的数据
|—-HashMap:作为Map的主要实现类;线程不安全的,效率高;能存储null的key和value
|—-LinkedHashMap:保证在遍历map元素时,可以照添加的顺序实现遍历。
|—-Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
|—-TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树
|—-Properties:常用来处理配置文件。key和value都是String类型
HashMap的底层:数组+链表 (jdk7及之前) 数组+链表+红黑树 (jdk8)

2.存储结构的理解:

Map中的key:无序的、不可重复的,使用Set存储所有的key —> key所在的类要重写equals()和hashCode() (以HashMap为例)
Map中的value:无序的、可重复的,使用Collection存储所有的value —>value所在的类要重写equals() 方法和 hashCode() 方法。
一个键值对:key-value构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所有的entry

Map 接口常用方法的测试`

添加:put(Object key,Object value)
删除:remove(Object key)
修改:put(Object key,Object value)
查询:get(Object key)
长度:size()
遍历:keySet() / values() / entrySet()

1.TreeMap的使用

向TreeMap中添加key-value,要求key必须是由同一个类创建的对象因为要照key进行排序:自然排序 、定制排序

Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素Object min(Collection)Object min(Collection,Comparator) int frequency(Collection,Object):返回指定集合中指定元素的出现次数

使用synchronizedList(List list) 和 synchronizedMap(Map map)包装不安全的集合

3.面试题:

面试题:Collection 和 Collections的区别?

Collections是工具类


13 泛型√

自定义泛型方法

方法在调用时才能确认泛型类型,声明方法时,在【修饰符】与返回值类型之间声明类型变量,我们把声明了类型变量的方法称为泛型方法

1
2
3
格式:修饰符<类型> 返回值类型 方法名(类型 变量名){}

示例:public <T> void show(T t){}
  1. 如果一个静态方法,使用泛型,必须要单独定义,而且要和类泛型不一致(即使是相同的字母,代表的也是不同的类型)
1
2
3
4
5
6
7
class GenericClass<T> {

// 静态方法必须单独定义泛型参数,即使是相同的字母
static <U> void print(U element) {
System.out.println(element);
}
}
数据类型确认时间
类的泛型 创建对象时
方法的泛型 调用方法时
泛型数组

不能使用new E[]。但是可以:E[] elements = (E[]) new Object[capacity];

子类继承父类时泛型的不同情况

edceec69140ff432a76b25d25f6107d

92fc862b020c502dc757ae43659034f

在这个例子中,Parent类是一个泛型类,Child1Child2Child3是它的子类。

  • Child1类是按需实现泛型,它继承自Parent<Integer>,在实例化时指定了具体的类型为Integer
  • Child2类完全保留了父类的泛型,它继承自Parent<T>,在实例化时指定了具体的类型为String
  • Child3类部分保留了父类的泛型,它继承自Parent<T>,并添加了一个额外的泛型类型U,在实例化时指定了TIntegerUString

泛型-泛型在继承上的体现及通配符的使用

A是类B的父类,但是G 和G二者不具备子父类关系,二者是并列关系,二者共同的父类是:G<?>

? extends A:
G<? extends A> 可以作为G
和G的子类,其中B是A的子类 ? super A:

G<? super A> 可以作为G和G的父类,其中B是A的父类

添加(写入):对于List<?>不能直接向其内部添加数据。(因为里面类型不确定)除了添加null之外,直到确定泛型类型

获取(读取):泛型允许读取数据,读取的数据类型为Object (因为通配符不知道具体类型,就当object了)。


14 数据结构与源码

1. 数据结构

1 数据的逻辑结构
  • 集合结构:“同属一个集合
  • 线性结构一对一
  • 树形结构一对多
  • 图形结构多对多
2 数据的存储结构(或物理结构)

数据的物理结构/存储结构:包括数据元素的表示关系的表示

结构1:顺序结构
结构2:链式结构
  • 节点中除了存放数据本身以外,还需要存放指向下一个节点的指针。一次申请一小块内存
  • 不支持下标访问和随机访问。
结构3:索引结构
  • 除建立存储节点信息外,还建立附加的索引表来记录每个元素节点的地址。
  • 缺点: 增加了附加的索引表,会占用较多的存储空间,在增加和删除数据时要修改索引表,因而会花费较多的时间。
结构4:散列结构
  • 根据元素的关键字直接计算出该元素的存储地址,又称为Hash存储。

  • 优点:检索、增加和删除结点的操作都很快。

  • 缺点:不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不能重复。

  • 在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型。

2 栈的特点

  • (Stack)又称为堆栈或堆叠,是限制仅在表的一端进行插入和删除运算的线性表。先进后出
  • 核心类库中的栈结构有Stack和LinkedList。

3. 队列

  • 队列(Queue)是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表。
  • 队列的修改原则:队列的修改是依先进先出(FIFO)的原则进行的。

5. 树与二叉树

1. 二叉树的基本概念

二叉树(Binary tree):二叉树特点是每个结点最多只能有两棵子树,且有左右之分。

2. 二叉树的遍历

哪个序就是根节点放哪的遍历

3. 经典二叉树

1满二叉树

v2-18006800f5ee46eb35929af957212dd1_1440w

2完全二叉树

img

3二叉排序/查找/搜索树
节点的左子树小于该节点本身,右子树大于该节点,每个节点都符合这样的规则

img

如果树本身不是均衡的会导致树退化成链表,这个时候所有操作的效率都是O(N)

4平衡二叉树:在二叉搜索树的基础之上添加自动维持平衡的性质,效率比较高为O(logN)

平衡二叉树的目的是为了减少二叉查找树的层次,提高查找速度。平衡二叉树的常用实现有红黑树、AVL、替罪羊树、Treap、伸展树等。

hioee6ng11

5红黑树
红黑树是一种自平衡二叉查找树,它的时间复杂度是O(logN),效率非常之高。


6. ArrayList 在 JDK7 和 JDK8 中的源码剖析

  • ArrayList默认扩容为原来的1.5倍,Vector默认扩容增加为原来的2倍。
  • ArrayList在JDK 6.0 及之前的版本也是10,JDK8.0 之后的版本ArrayList初始化为长度为0的空数组,之后在添加第一个元素时,再创建长度为10的数组。

2.链表LinkedList

链表与动态数组的区别

链表底层的物理结构是链表,因此根据索引访问的效率不高,即查找元素慢。但是插入和删除不需要移动元素,只需要修改前后元素的指向关系即可,所以插入、删除元素快。而且链表的添加不会涉及到扩容问题。

哈希表的物理结构

HashMap和Hashtable底层都是哈希表(也称散列表),其中······ 你添加的映射关系(key,value)最终都被封装为一个Map.Entry类型的对象,放到某个table[index]桶中。
使用数组的目的是查询和添加的效率高,可以根据索引直接定位到某个table[index]。


hashmap底层

使用HashMap()的构造器创建对象时,并没有在底层初始化长度为16的table数组。


jdk8中添加的key,value封装到了HashMap.Node类的对象中。而非jdk7中的HashMap.Entry,key-value被封装为HashMap.Node类型或HashMap.TreeNode类型,它俩都直接或间接的实现了Map.Entry接口。即table[index]下的映射关系可能串起来一个链表或一棵红黑树


jdk8中新增的元素所在的索引位置如果有其他元素。在经过一系列判断后,如果能添加,则是旧的元素指向新的元素。而非jdk7中的新的元素指向旧的元素。“七上八下”


jdk7时底层的数据结构是:数组+单向链表。 而jdk8时,底层的数据结构是:数组+单向链表+红黑树。
红黑树出现的时机:当某个索引位置i上的链表的长度达到8,且数组的长度超过64时,此索引位置上的元素要从单向链表改为红黑树。
如果索引i位置是红黑树的结构,当不断删除元素的情况下,当前索引i位置上的元素的个数低于6时,要从红黑树改为单向链表。

Set底层

Set的内部实现其实是一个Map,Set中的元素,存储在HashMap的key中。即HashSet的内部实现是一个HashMap,TreeSet的内部实现是一个TreeMap,LinkedHashSet的内部实现是一个LinkedHashMap。

3.【拓展】HashMap的相关问题

3.1说说你理解的哈希算法

hash算法是一种可以从任何数据中提取出其“指纹”的数据摘要算法,它将任意大小的数据映射到一个固定大小的序列上,这个序列被称为hash code、数据摘要或者指纹。比较出名的hash算法有MD5、SHA。hash是具有唯一性且不可逆的,唯一性是指相同的“对象”产生的hash code永远是一样的。

3.2Entry中的hash属性为什么不直接使用key的hashCode()返回值呢?

不管是JDK1.7还是JDK1.8中,都不是直接用key的hashCode值直接与table.length-1计算求下标的,而是先对key的hashCode值进行了一个运算,JDK1.7和JDK1.8关于hash()的实现代码不一样,但是不管怎么样都是为了提高hash code值与 (table.length-1)的按位与完的结果,尽量的均匀分布。

3.3HashMap是如何决定某个key-value存在哪个桶的呢?

因为hash值是一个整数,而数组的长度也是一个整数,有两种思路:
①hash 值 % table.length会得到一个[0,table.length-1]范围的值,正好是下标范围,但是用%运算效率没有位运算符&高。
②hash 值 & (table.length-1),任何数 & (table.length-1)的结果也一定在[0, table.length-1]范围。

3.4为什么要保持table数组一直是2的n次幂呢?

因为如果数组的长度为2的n次幂,那么table.length-1的二进制就是一个高位全是0,低位全是1的数字,这样才能保证每一个下标位置都有机会被用到。

为什么JDK1.8会出现红黑树和链表共存呢?

结构简单用链表,复杂用红黑树

因为当冲突比较严重时,table[index]下面的链表就会很长,那么会导致查找效率大大降低,而如果此时选用二叉树可以大大提高查询效率。
但是二叉树的结构又过于复杂,占用内存也较多,如果结点个数比较少的时候,那么选择链表反而更简单。所以会出现红黑树和链表共存。

key-value中的key是否可以修改?

key-value存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的key-value,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。
这个规则也同样适用于LinkedHashMap、HashSet、LinkedHashSet、Hashtable等所有散列存储结构的集合。


14 File类与IO流

image.png

蓝框的流需要大家重点关注。

①read()的理解:返回读入的一个字符。如果达到文件末尾,返回-1
②如果流使用的构造器是:FileWriter(file,true):不会对原文件覆盖

判断fw和fr是否为null,可以保证在资源初始化或关闭的过程中,不会因为空指针而导致程序异常

(字节流可以处理一切文件)

提高读写速度的原因:内部提供了一个缓冲区。默认情况下是8kb

1
2
说明1:先关闭外层的流,再关闭内层的流
说明2:关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略.

InputStreamReader:将一个字节的输入流转换为字符的输入流
解码:字节、字节数组 —>字符数组、字符串
OutputStreamWriter:将一个字符的输出流转换为字节的输出流
编码:字符数组、字符串 —> 字节、字节数组
说明:编码决定了解码的方式

1
2
ObjectOutputStream:内存中的对象--->存储中的文件、通过网络传输出去:序列化过程`
`ObjectInputStream:存储中的文件、通过网络接收过来 --->内存中的对象:反序列化过程

补充:ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量

数据流:

DataInputStream 和 DataOutputStream

随机存取文件流:RandomAccessFile

RandomAccessFile既可以作为一个输入流,又可以作为一个输出流


15 网络编程

TCP/IP协议四层:

应用层,传输层,网络层,物理+数据链路层

TCP与UDP

TCP:使用前先建立TCP连接,可靠,重发机制,大数据量,效率低

UDP:

不需要建立连接,收到不确认,不可靠,数据64k内,效率高

三次握手,四次挥手

16 反射

反射机制-类的加载过程与类加载器的理解

image-20220417173459849.png

17 Java新特性

四大核心函数式接口

函数式接口 称谓 参数类型 用途
Consumer 消费型接口 T 对类型为T的对象应用操作,包含方法: void accept(T t)
Supplier 供给型接口 返回类型为T的对象,包含方法:T get()
Function<T, R> 函数型接口 T 对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t)
Predicate 判断型接口 T 确定类型为T的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t)
1
2
3
//用于删除集合中满足filter指定的条件判断的。
//删除包含o字母的元素
list.removeIf(s -> s.contains("o"));
1
Function<String,String> fun = s -> s.substring(0,1).toUpperCase() + s.substring(1);

上面是四种类型的实例

方法引用&构造器引用

方法引用
  • 格式:使用方法引用操作符 “::” 将类(或对象) 与 方法名分隔开来。
    • 两个:中间不能有空格,而且必须英文状态下半角输入
  • 如下三种主要使用情况
    • 情况1:对象 :: 实例方法名
    • 情况2:类 :: 静态方法名
    • 情况3:类 :: 实例方法名
构造器引用

当Lambda表达式是创建一个对象,并且满足:Lambda表达式形参,正好是创建这个对象的构造器的实参列表,就可以使用构造器引用。
格式:

类名::new

Stream流式编程

创建

4种方式创建stream流

方式一:通过集合

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

  • default Stream stream() : 返回一个顺序流
  • default Stream parallelStream() : 返回一个并行流
1
Stream<Integer> stream = list.stream(); 
方式二:通过数组
1
Arrays.stream(arr);
方式三:通过Stream的of()

可以调用Stream类静态方法 of(), 通过显示值创建一个流。它可以接收任意数量的参数。

  • public static Stream of(T… values) : 返回一个流
1
2
3
4
5
@Test
public void test04(){
Stream<Integer> stream = Stream.of(1,2,3,4,5);
stream.forEach(System.out::println);
}
方式四:创建无限流(了解)
中间操作
筛选与切片
方 法 描 述
filter(Predicatep) 接收 Lambda , 从流中排除某些元素
distinct() 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
limit(long maxSize) 截断流,使其元素不超过给定数量
skip(long n) 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
映 射
方法 描述
map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
mapToInt(ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。
flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
排序
方法 描述
sorted() 产生一个新流,其中按自然顺序排序
sorted(Comparatorcom)
其它中间操作······
终止操作

包括匹配与查找、归约、收集

方法 描述
boolean allMatch(Predicate p) 检查是否匹配所有元素
boolean anyMatch(Predicate p) 检查是否至少匹配一个元素
boolean noneMatch(Predicate p) 检查是否没有匹配所有元素
Optional findFirst() 返回第一个元素
long count() 返回流中元素总数
Optional max() 返回流中最大值
Optional min() 返回流中最小值
void forEach(Consumer c) 迭代