java基础知识

文章目录

  • 1. 数据结构
    • Hashmap的get和put方法的实现?
    • 在初始化和扩容时,ArrayList的过程如下:
    • 要证明HashMap不是线程安全的,可以通过以下步骤进行实验:
  • 2.流
  • 3.线程池 多线程
  • 3.1线程
    • 3.2 线程池
  • 4.锁
  • 5.面向对象
    • 5.2 封装、继承、多态
    • 5.2抽象、接口
    • 5.3重写 、 重载
    • 5.4final
    • 5.5创建对象的方式
  • 6.设计模式
    • 6.1 单例设计模式
    • 6.2工厂模式(Factory Pattern)
    • 6.3策略模式(Strategy Pattern)
  • 7.反射
  • 8.异常
  • 9.常用类
    • 9.1 String
    • 9.2 Object
    • 9.3 数组
  • 10 其他
    • Linux基础
    • cookie / session的区别
    • 转发 、 重定向的区别
    • http与https 的区别

1. 数据结构

java的数据结构有哪些?
线性结构:数组、链表、哈希表;队列、栈
非线性结构有:堆、树(二叉树、B树、B+树、红黑树)

常用的集合类有List集合,Set集合,Map集合,其中List集合与Set集合继承了Collection接口,
java基础知识_第1张图片
List有序可重复的集合接口,继承自Collection接口,表示元素按照插入顺序排列。
Set无序不重复的集合接口,继承自Collection接口,表示元素唯一性。
Map键值对映射的集合接口,表示具有唯一键和对应值的集合。
集合的遍历:可以使用迭代器(Iterator)来遍历集合中的元素,也可以使用增强型for循环(foreach)来简化遍历操作。迭代器提供了对集合中元素的统一访问方式。
1.List、Set 和 Map 之间有什么区别?它们的常用实现类有哪些?

List、Set和Map是Java集合框架中的三个核心接口,它们之间的区别如下:

List:
list是有序可重复的集合接口,可以按照顺序或者指定的顺序访问和操作元素,还可以通过索引访问元素
常用实现类:ArrayList、LinkedList
Set:
set是无序不重复的集合接口,不能通过索引访问元素。
常用实现类:HashSet、TreeSet
Map:
map是键值对映射的集合接口,每个键只能对应一个值。键值均可以为空
常用实现类:HashMap、ConcurrentHashMap

3.ArrayList 和 LinkedList 的区别是什么?它们适用于不同的场景吗?

ArrayList:底层是数组,查询快增删慢,可以通过索引随机访问,时间复杂度是O(1),
LinkedList: 底层是链表,查询慢增删块,不能使用索引访问,需要从头结点或者尾结点开始遍历,时间复杂度为O(n)

ArrayList适用于需要快速随机访问元素的场景,例如需要频繁地根据索引读取或修改元素的情况。
LinkedList适用于需要频繁进行插入和删除操作的场景,特别是在中间或开头进行插入和删除操作比较多的情况。
对于大部分常见的情况,ArrayList的性能要优于LinkedList,因为数组的访问速度更快。
但在某些特定的场景下,LinkedList可能会更适合,例如需要频繁进行插入和删除操作,并且对于随机访问的性能要求较低的情况。

4.HashSet 和 TreeSet 的区别是什么?它们如何保证元素的唯一性?
HashSet的底层是hash表且是无序不重复的集合接口
TreeSet的底层是红黑树,是有序不重复的集合接口

为了保证元素的唯一性,HashSet和TreeSet在判断元素是否重复时,依赖于元素的equals方法(和哈希码)

在选择HashSet和TreeSet时,需要根据具体的需求进行选择:
如果只关心元素的唯一性,而不关心元素的顺序,可以选择HashSet,它的插入、删除和查找操作的性能较好。
如果需要对元素进行排序,可以选择TreeSet,它会根据元素的顺序进行存储,但由于需要维护红黑树的平衡性,插入、删除和查找操作的性能稍低于HashSet。
总结:HashSet和TreeSet都可以保证元素的唯一性,HashSet适用于无序需求,而TreeSet适用于有序需求。

5.HashMap 和 HashTable 的区别是什么?它们的线程安全性如何?
HashMap是非线程安全的,存储的键和值都可以为null
而HashTable是线程安全的,存储的键和值都不可以为null
HashMap的性能通常比HashTable更好。

6.ConcurrentHashMap 是如何实现线程安全的?它与 HashMap 的区别是什么?
实现线程安全的几个关键点:
ConcurrentHashMap内部使用了分段式的锁,将整个数据结构分成一些独立的部分,并称为“段”并对不同的段进行了锁的力度的控制。还使用了volatile关键字来确保可见性,确保当一个线程修改了某个段的内容后,其他线程可以立即看到修改的结果。还使用并发安全的数据结构(hashEntry数组和链表)与线程安全的迭代器

与HashMap相比,ConcurrentHashMap的区别如下
ConcurrentHashMap是线程安全的,可以在多线程环境下进行并发操作,而HashMap是非线程安全的。在并发环境下,ConcurrentHashMap的性能通常比HashMap好,因为它通过分段锁的机制允许多个线程同时进行读操作,提高了并发性能。

7.如何遍历集合框架中的元素?有哪些遍历方式?

1.使用迭代器(Iterator):通过调用集合的iterator()方法获取迭代器对象,然后使用while循环和next()方法逐个访问元素,直到遍历完所有元素。


List<String> list = new ArrayList<>();
// 添加元素到列表中
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    // 处理元素
}

2.使用增强型for循环(foreach):适用于数组和实现了Iterable接口的集合类,可以直接通过for循环遍历元素,不需要显式使用迭代器。


List<String> list = new ArrayList<>();
// 添加元素到列表中
for (String element : list) {
    // 处理元素
}

3.**使用Lambda表达式和Stream API:**从Java 8开始,引入了Lambda表达式和Stream API,可以通过Stream的forEach()方法对集合进行遍历,并结合Lambda表达式进行元素处理。

List<String> list = new ArrayList<>();
// 添加元素到列表中
list.stream().forEach(element -> {
    // 处理元素
});

4.使用普通的for循环:适用于数组和实现了RandomAccess接口的集合类,通过下标访问元素进行遍历。


List<String> list = new ArrayList<>();
// 添加元素到列表中
for (int i = 0; i < list.size(); i++) {
    String element = list.get(i);
    // 处理元素
}

8.collection与collections的区别

Collection 是一个集合接口
Collection,提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
Collections 是一个包装类
Collections,是一个工具类,它包含了很多静态方法,不能被实例化,比如排序方法: Collections. sort(list)等。

9.如何实现数组和 List 之间的转换?

数组转 List:使用 Arrays. asList(array) 进行转换。
List 转数组:使用 List 自带的 toArray() 方法。

10.HashMap的实现原理:

HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储,get(key)来获取。当传入 key 时,HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树

11.线程安全的集合
1.常见的集合中的线程安全集合
java基础知识_第2张图片

**List**

1.Vector
原理是为其所有需要线程安全的方法都添加了synchronize关键字,锁住了整个对象(使用的互斥锁)
2.CopyOnWriteArrayList
在多线程中,读操作跟普通的ArrayList没有区别,写操作会上锁,上锁后将数据复制一份,再将数据写入,避免数据覆盖而造成的数据问题(使用读写锁)

**Set**

1.CopyOnWriteArraySet
底层使用的是CopyOnWriteArrayList

**Queue**

1.ConcurrentListedQueue
线程安全,读写效率高的队列,高并发情况下性能最好
其使用CAS比较交换算法来实现线程安全,其添加对象时涉及三个核心参数(V,E,N)
V:当前需要更新的变量,E:预期值,N:新值
只有当V=E时,才会将V=N,否则表示已经被别的线程更新,取消当前操作
2.BlockingQueue
https://blog.csdn.net/sjemYele/article/details/121004818

ArrayBlockingQueue
LinkedBlockingQueue

2.Map中线程安全的

concurrentHashMap
是一个支持高并发更新与查询的哈希表(基于HashMap)。
在保证安全的前提下,进行检索不需要锁定。
HashTable
原理是为每个方法添加了synchronized关键字,来实现的线程安全,锁住了整个对象(使用的锁是互斥锁)

Hashmap的get和put方法的实现?

HashMap 是基于哈希表实现的键值对存储结构,其中的 get() 和 put() 方法用于获取和插入键值对。下面是它们的简要实现过程:

  1. get() 方法的实现:

    • 首先,根据传入的键对象使用哈希函数计算出哈希值。
    • 哈希值用于确定键值对在内部数组中的索引位置。
    • 根据索引位置,在数组中找到对应的存储桶(bucket)。
    • 若存储桶为空,返回 null,表示没有找到对应的键值对。
    • 若存储桶不为空,则遍历存储桶中的链表或红黑树(Java 8+),查找与传入键对象相等的键值对。
    • 若找到匹配的键值对,则返回对应的值;否则返回 null。
  2. put() 方法的实现:

    • 首先,根据传入的键对象使用哈希函数计算出哈希值。
    • 哈希值用于确定键值对在内部数组中的索引位置。
    • 根据索引位置,在数组中找到对应的存储桶。
    • 若存储桶为空,直接将键值对插入存储桶中,并增加 HashMap 的大小计数器。
    • 若存储桶不为空,则遍历存储桶中的链表或红黑树,检查键对象是否已存在。
    • 若存在相同的键对象,则更新对应的值。
    • 若不存在相同的键对象,则将键值对添加到链表或红黑树中。
    • 如果链表或红黑树的长度超过了阈值(8),则将链表转换为红黑树,提高查找效率。
    • 若 HashMap 的大小达到了容量的阈值(负载因子 * 数组长度),则进行扩容操作,重新计算哈希值和存储位置。

需要注意的是,HashMap 采用数组+链表(或红黑树)的方式来解决哈希冲突,即不同的键对象可能会计算得到相同的哈希值。因此,在查找和插入过程中,需要遍历链表或红黑树来确保准确的键值对匹配。Java 8 引入了红黑树的优化,使得在一些场景下查找效率更高。

以上是对 HashMap 的 get() 和 put() 方法的简要实现过程的描述,实际的实现可能会有更多的细节和优化。

在初始化和扩容时,ArrayList的过程如下:

  1. 初始化过程:

    • 创建一个ArrayList对象时,会分配一个默认大小的内部数组(底层实现是Object数组)。
    • 默认情况下,ArrayList的初始容量为10(可以通过构造函数设置初始容量)。
    • 初始化后,ArrayList的元素个数为0。
  2. 扩容过程:

    • 当向ArrayList中添加元素时,如果当前元素个数已经达到了内部数组的容量上限,则需要进行扩容操作。
    • 扩容时,ArrayList会创建一个新的更大的数组,并将原数组中的元素复制到新数组中。
    • 扩容的策略是当前容量不足时,将容量增加为原来的1.5倍(可以通过构造函数或方法设置扩容因子)。
    • 在Java 7及以前的版本中,扩容是直接创建一个新的数组并进行复制操作。
    • 在Java 8及以后的版本中,扩容是通过使用Arrays.copyOf方法来创建新数组并进行复制操作。

扩容过程可能会引起性能开销,因为需要重新创建数组和复制元素。为了避免频繁的扩容操作,可以在创建ArrayList时设置一个合适的初始容量,以减少扩容的次数。

另外,需要注意的是,在多线程环境下,对ArrayList进行初始化和扩容操作时可能会出现线程安全问题。如果需要在多线程环境下使用ArrayList,可以考虑使用线程安全的替代类,如CopyOnWriteArrayList或使用适当的同步措施来保证线程安全性。

要证明HashMap不是线程安全的,可以通过以下步骤进行实验:

  1. 创建多个线程:创建多个并发线程,每个线程都执行对HashMap的并发读写操作。

  2. 并发写操作:在每个线程中,同时进行对HashMap的写操作,例如插入元素或修改元素的值。

  3. 并发读操作:同时在其他线程中进行对HashMap的读操作,例如获取元素值或进行遍历操作。

  4. 观察结果:观察程序运行过程中是否出现以下情况:

    • 线程之间发生竞态条件,导致数据的覆盖或丢失。
    • 程序抛出ConcurrentModificationException异常,表示并发修改异常。
    • 读操作获取到不一致的结果,即读取到了写操作尚未完成的中间状态数据。

如果在实验中出现了上述情况,即表明HashMap不是线程安全的数据结构。这是因为HashMap的内部结构并没有提供对并发访问的保护机制,多个线程同时进行读写操作可能会导致数据不一致或抛出异常。

要解决HashMap的线程安全问题,可以考虑以下方法:

  • 使用ConcurrentHashMapConcurrentHashMap是Java提供的线程安全的HashMap的替代类,它使用了分段锁(Segment)来实现并发访问的安全性。
  • 使用同步机制:通过在对HashMap的访问代码块使用同步措施,如synchronized关键字或ReentrantLock,可以确保在同一时间只有一个线程访问HashMap,从而避免竞态条件和不一致性的问题。

总结起来,通过实验观察并发读写操作过程中是否发生数据不一致或异常抛出,可以证明HashMap不是线程安全的。

2.流

IO流根据流向分为 输入流和输出流
根据类型分为字节流与字符流,字节流用于处理二进制数据,以字节为单位进行读写操作;字符流用于处理文本数据,以字符为单位进行读写操作。
字节流:InputStream、OutputStream、FileInputStream、FileOutputStream等。
字符流:Reader、Writer、FileReader、FileWriter等。
还有缓冲流,他是基于字节流或字符流的装饰器,通过在内存中设置缓冲区来提高读写的效率。缓冲流可以减少对底层流的频繁读写操作,从而提高性能。常用的缓冲流类有:
字节缓冲流:BufferedInputStream、BufferedOutputStream。
字符缓冲流:BufferedReader、BufferedWriter。

3.线程池 多线程

3.1线程

1.创建线程有三种方式

1.继承Thread,重写run方法
2.实现Runable接口,重写run方法
3.实现Callable接口,重写call方法

2.Runnable与callable区别

Runnable规定的方法是run(),Callable规定的是call()
Runnable 的任务执行后无返回值,Callable的任务执行后有返回值
call()方法可以抛出异常,run()方法不可以,因为run()方法本身没有抛出异常,所以自定义的线程类在重写run()方法的时候也无法抛出异常
运行Callable 任务可以拿到一个Future对象,表示异步计算的结果。

3.线程的状态
五态:1.新建2.就绪3.运行4.阻塞5.终止
七态:1.新建2.就绪3.运行4.阻塞5.等待6.超时等待7.终止

线程生命周期,主要有五种状态:

当线程创建后进入新建状态,调用start()方法进入就绪状态cup分配时间片,拿到分配的时间片后进入运行状态。

新建状态(New) : 当线程对象创建后就进入了新建状态.如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法,线程即为进入就绪状态.
运行状态(Running):当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态
阻塞状态(Blocked):处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.
根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:
等待阻塞:运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态
同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期

3.2 线程池

线程池的创建有七个参数,分别是:核心线程数,最大线程数,工作队列,线程工厂,存活时间,存活时间单位,拒绝策略

1. 线程池的工作流程

接收到任务,首先判断一下核心线程是否已满,如果未满,则去创建一个新的线程执行任务,如果核心线程数已满,工作对列未满,将线程存储到工作对列中,等待核心线程获取执行;如果工作对列已满且线程数小于最大线程数,则创建一个新的线程线程处理任务(需要获取全局锁);如果线程数超过了最大线程数,按照四种拒绝策略处理任务,四种拒绝策略有:1.提交任务的线程自己去执行该任务 2.默认拒绝策略,会抛出异常 3.直接丢弃任务,没有任何异常抛出4.丢弃最老的任务,其实就是把最早进入工作队列丢弃,然后把新任务加入到工作对列

2. 三种常见的线程池

创建线程池的方式有多种,我了解到的有,1.固定大小的线程池,可控制并发的线程数,超出的线程会在工作队列中等待。2.带有缓存的线程池。3,可以执行延迟任务的线程池

2.为什么使用线程池

线程池可以降低线程生命周期的系统开销问题,加快响应速度;统筹内存和CPU的使用,避免资源使用不当;可以统一管理资源

4.锁

1. 锁的类型
锁分为乐观锁、悲观锁、synchronized
乐观锁是每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
悲观锁每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放
2…lock和synchronize的区别synchronized
都是解决线程安全的工具,synchronize是java中的同步关键字;而lock是J.U.C包中提供的接口
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。

2.lock和synchronize的区别

这个问题我从四个方面来回答
第一个,从功能角度来看,lock和synchronize都是java中用来解决线程安全问题的一个工具,
第二个,从特性来看,首先synchronize是java中的同步关键字;而lock是J.U.C包中提供的接口而这个接口它有很多的实现类其中就包括reentrantLock这样一个重入锁的实现,其次synchronize可以通过两种方式去控制锁的力度,一种是把synchronize关键字修饰在方法层面,另一张种是修饰在代码块上,并且我们可以通过synchronize加锁对象的生命周期来控制锁的作用范围,比如锁对象是静态对象或者类对象那么这个锁就属于全局锁;如果锁对象是普通实例对象,那么这个锁的范围取决于这个实例的生命周期。lock中锁的力度是通过它里面提供的lock( )方法和unlock( )方法来决定的,包裹在两个方法之间的代码是可以保证线程安全的,而锁的作用域取决于lock实例的生命周期。
lock比synchronize的灵活性更高,lock可以自主的去决定什么时候加锁,什么时候释放锁,只需要调用lock()和unlock()方法就可以了。同时lock还提供了非阻塞的竞争锁的方法,叫trylock(),这个方法可以通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁,而synchronize由于是关键字所以他无法去实现非阻塞竞争锁的方法,另外synchronize锁的释放是被动的,就是当synchronize同步代码块执行结束以后或者代码出现异常的时候才会被释放。
最后lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时候如果已经有其他线程正在排队或者等待锁释放那么当前竞争锁的线程是无法插队的;而非公平锁就是不管是否有线程在排队等待锁他都会去尝试竞争一次锁,synchronize只提供了一种非公平锁的实现。
第三个,从性能方面来看,synchronize和lock在性能方面相差不大。在实现上会有一定的区别,synchronize引入了偏向锁,轻量级锁,重量级锁,以及锁升级的机制去实现锁的优化,而lock中用到了自旋锁的方式,去实现性能优化,以上就是我对这个问题的理解。

Sychronized关键字的理解?
synchronized是Java中的关键字,用于实现线程同步和对共享资源的互斥访问。它可以应用于方法和代码块。

使用synchronized关键字可以确保在同一时间只有一个线程可以访问被Synchronized修饰的方法或代码块,从而保证线程安全性。

synchronized关键字的主要特点和理解如下:
1.线程互斥:使用synchronized关键字修饰的代码块,在同一时间只能被一个线程执行。其他线程需要等待当前线程执行完毕才能访问
2.对象锁:synchronized关键字是基于对象的锁机制实现的。每个对象都有一个关联的锁(也称为监视器或互斥锁),当线程进入synchronized代码块时,它会尝试获取对象的锁,如果锁已被其他线程持有,则当前线程进入阻塞状态等待锁释放。
3.保证可见性和有序性:进入synchronized代码块会建立一个内存屏障,它能够确保线程在进入和退出临界区时的内存状态得到同步,从而保证了可见性和有序性。
4.重入性:同一线程可以多次获取同一个对象的锁,即支持重入性。当线程已经持有锁时,再次进入synchronized代码块不会被阻塞,而是可以继续执行。

synchronized关键字可以应用于不同的层级:

  • 实例方法:使用synchronized修饰的实例方法,锁的范围是当前实例对象。
  • 静态方法:使用synchronized修饰的静态方法,锁的范围是当前类的Class对象,即类锁。
  • 代码块:使用synchronized修饰的代码块,需要指定锁的对象,锁的范围是指定对象。

通过合理地使用synchronized关键字,可以确保多个线程之间对共享资源的安全访问,避免竞态条件和数据不一致的问题。但需要注意,过度使用synchronized可能会导致性能问题,因此在使用时应考虑到锁的粒度和效率。此外,Java还提供了其他并发工具类,如ReentrantLockLock接口,可以提供更灵活的线程同步机制。
3.说一下死锁
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
产生死锁的原因(1) 因为系统资源不足。(2) 进程运行推进的顺序不合适。(3) 资源分配不当等。
怎样防止死锁
1、尽量使用try lock( )防范,设置超时时间,超时则关闭,防止死锁
2、使用安全类concurrent 代替自己手写锁
3、减少锁的使用粒度,避免几个功能共用一把锁
4、减少同步代码块
4.有这样一个场景:“当得到第一个线程的结果时,直接终止掉其他线程”,怎么实现?
要实现当得到第一个线程的结果时,直接终止其他线程,可以使用CountDownLatchCyclicBarrier来协调线程的执行。下面是一种基本的实现方式:

  1. 创建一个CountDownLatch对象,并将计数器初始化为1。

  2. 创建多个线程,并将CountDownLatch对象作为参数传递给这些线程。

  3. 在每个线程的执行逻辑中,首先进行需要执行的任务,并在任务执行完后,调用CountDownLatchcountDown()方法来减少计数器的值。

  4. 在主线程中,调用CountDownLatchawait()方法来等待计数器归零。一旦计数器归零,表示已经有一个线程执行完毕,此时可以终止其他线程的执行。

以下是一个简单的示例代码:

import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) {
        int threadCount = 5; // 线程数量
        CountDownLatch latch = new CountDownLatch(1); // 创建CountDownLatch对象,计数器初始值为1

        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new WorkerThread(latch)); // 创建线程,并传入CountDownLatch对象
            thread.start(); // 启动线程
        }

        // 主线程等待计数器归零
        try {
            latch.await();
            System.out.println("得到第一个线程的结果,终止其他线程");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class WorkerThread implements Runnable {
        private CountDownLatch latch;

        public WorkerThread(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void run() {
            // 执行需要的任务
            // ...

            // 减少计数器的值
            latch.countDown();
        }
    }
}

在上述示例中,当任何一个线程执行完任务后调用latch.countDown()来减少计数器的值,主线程通过latch.await()等待计数器归零,一旦归零则输出"得到第一个线程的结果,终止其他线程"。这样就可以实现当得到第一个线程的结果时,直接终止其他线程的执行。

5.面向对象

5.2 封装、继承、多态

1.谈谈对面向对象的理解

面向对象 ( Object Oriented ) 是将现实问题构建关系,然后抽象成 类 ( class ),给类定义属性和方法后,再将类实例化成 实例 ( instance ) ,通过访问实例的属性和调用方法来进行使用。面向对象有三大特性,封装、继承和多态。封装隐藏了类的内部实现机制,可以在不影响使用功能的情况下改变类的内部结构,同时也保护了数据。隐藏内部细节,只暴露给外界访问方法。继承,子类继承父类,表明子类是特殊的父类,并且拥有父类不具有的一些属性或方法,Java通过extends关键字实现继承,父类中通过private 定义的变量和方法不会被继承,子类不可以直接操作父类私有的(private)变量和方法,多态指的是类和类的关系,两个类由继承关系存在有方法的重写,因此可以调用父类引用指向子类对象。多态必备三要素,继承,重写,父类引用指向子类对象

2.继承

通过extends关键字来构成继承关系
子类无法继承父类的私有方法,无法继承构造方法
子类在创建对象时,默认会先调用父类的构造方法
支持单继承,具有传递性,耦合性非常强
子类可以修改父类的功能,也可以拓展自己的功能

3.封装
把相关的数据封装成一个类组件,仅对外提供操作数据的方法

4.多态

多态的前提是继承+重写、父类引用指向子类对象
多态对象使用的成员变量是父类的
若使用的方法被重写了则使用的是子类的方法
静态资源 调用谁的就是谁的
不用关心某个对象是什么类型的,就可以直接使用其方法

5.2抽象、接口

1.抽象

Java中被abstract关键字修饰的方法叫抽象方法,只有声明没有方法体。
被abstract关键字修饰的类叫抽象类,可以不包含抽象方法,不可被实例化
子类继承抽象类之后,a.继续抽象,b.重写父类的所有抽象方法
abstract不能与private连用,子类无法重写
abstract不能与static连用,存在加载顺序的问题
abstract不能与final连用,无法重写

2.接口

被interface修饰的类叫接口,理解为是一种特色的类,由全局常量和公共的抽象方法所组成。通过implements来实现接口,接口与类之间可以多实现,接口与接口之间可以实现多继承,降低了耦合性

3.接口与抽象类的区别

实现:抽象类的子类使用extends来继承;接口必须使用implements来实现接口
构造函数:抽象类可以有构造函数;接口没有
实现数量:类可以实现很多个接口;但是只能继承一个抽象类
访问修饰符:接口中的方法默认使用public;抽象类中的方法可以是任意访问修饰符
何时使用
需要为一些类提供公共的实现代码时,应优先考虑抽象类
当注重代码的扩展性跟可维护性时,应当优先采用接口

5.3重写 、 重载

1.重写
重写也可以看做覆盖,子类重新定义父类中具有相同名称和参数的虚函数,函数特征相同,但函数的具体实现不同,它主要在继承关系中出现

1.继承以后,子类就拥有了父类的功能。
2.在子类中,可以添加子类特有的功能,也可以修改父类原有的功能。
3.子类中方法的签名与父类完全一致时,会发生覆盖/复写的现象
4.父类的私有方法不能被重写
5.重写的要求是:两同两小一大

两同:方法名 参数列表完全一致
两小:

子类返回值类型小于等于父类的返回值类型
子类抛出异常小于等于父类方法抛出异常

一大:子类方法的修饰符权限要大于等于父类被重写方法的修饰符权限

2.重载
重载是函数名相同,参数列表不同,重载只是在类的内部存在,但是不能返回类型来判断。

1.重载overload与重写override的区别

重载:是一个类中的现象,同一个类中存在方法名相同,参数列表不同的方法
重写:是指建立了继承关系之后,子类对父类的方法不满意,可以重写,遵循两同两小一大的原则
重载的意义:是为了外界调用方法时方便,不管传入什么样的参数,都可以匹配到对应的同名方法
重写的意义:在不修改源码的情况下,进行功能的修改与拓展(OCP原则:面向修改关闭,面向拓展开放)

5.4final

1.final 的作用

被final修饰的类是最终类,不能被继承
被final修饰的方法是最终实现,不可被重写
被final修饰的变量是常量必须初始化,且不可被改变

2.被final修饰的属性是否可以被修改?为什么
不可以

5.5创建对象的方式

new
反射
clone
反序列化

6.设计模式

6.1 单例设计模式

介绍:单例模式是一种设计模式,旨在确保一个类只有一个实例,并提供全局访问点来访问该实例。这种模式通常用于需要共享资源的场景,以确保只有一个实例存在,并提供对该实例的统一访问方式。
如何使用
创建单例模式通常需要考虑以下几个要点:

  1. 私有化构造函数:单例类的构造函数需要被私有化,这样外部就无法通过实例化来创建新的对象。
  2. 静态成员变量:需要在单例类中定义一个静态成员变量来保存单例实例。这个变量需要被声明为私有的,并且是类的唯一实例。
  3. 静态获取方法:提供一个静态方法来获取单例实例。这个方法会检查静态成员变量是否已经被初始化,如果是,则直接返回该实例;如果否,则创建一个新的实例,并将其赋值给静态成员变量。

下面是一个简单的示例

package com.sbxBase.chenTest;

public class Logger {
    private static Logger instance; //静态成员变量
    private String log;

    private Logger() {
        // 私有化构造函数
        log = "";
    }

    /**
     * 静态获取方法
     * @return  如果不存在则返回一个新建的对象,如果存在则返回自己
     */
    public static Logger getInstance() {
        if (instance == null) {
            synchronized (Logger.class) {
                if (instance == null) {
                    instance = new Logger();
                }
            }
        }
        return instance;
    }

    public void logMessage(String message) {
        log += message + "\n";
    }

    public void printLog() {
        System.out.println("Log:\n" + log);
    }


    public static void main(String[] args) {
        // 获取单例实例
        Logger logger = Logger.getInstance();

        // 记录日志
        logger.logMessage("Error: File not found.");
        logger.logMessage("Warning: Memory low.");

        // 打印日志
        logger.printLog();
    }


}

在上述示例中,Logger 类被设计为一个单例类。通过 getInstance() 方法获取 Logger 类的唯一实例。在 getInstance() 方法中使用了双重检查锁定,以确保在多线程环境下仍然是线程安全的。
在 Main 类的 main 方法中,我们首先获取 Logger 实例,然后调用 logMessage 方法记录一些日志信息,最后调用 printLog 方法打印日志。
这样,我们就能通过 Logger.getInstance() 方法获得全局唯一的 Logger 实例,并且可以通过该实例记录和打印日志信息。

6.2工厂模式(Factory Pattern)

介绍:
工厂模式是一种创建型设计模式,旨在提供一种统一的方式来创建对象,而无需直接暴露对象的创建逻辑。它通过定义一个工厂类,该工厂类负责根据客户端的请求创建相应的对象,并将其返回给客户端使用。
如何使用
在工厂模式中,通常会定义一个抽象的产品类或接口,表示可以创建的对象类型。然后,具体的产品类将继承或实现该抽象类或接口,并实现自己的特定逻辑。工厂类将根据客户端的需求,创建对应的具体产品对象并返回。

下面是一个使用工厂模式创建单例对象的示例,其中我们假设有一个日志记录器(Logger)的抽象类和两个具体的日志记录器类:FileLoggerDatabaseLogger。我们将使用工厂模式来创建单例的日志记录器对象。

package com.sbxBase.chenTest;

/**
 * 定义的抽象类,每个实例类都会提供的方法
 */
public abstract class Logger {
    public abstract void logMessage(String message);
}

/**
 * 具体的实例类  文件日志类
 */
 class FileLogger extends Logger {
    private static FileLogger instance;
    private String logFile;

    private FileLogger() {
        // 私有化构造函数
        logFile = "log.txt";
    }

    public static FileLogger getInstance() {
        if (instance == null) {
            synchronized (FileLogger.class) {
                if (instance == null) {
                    instance = new FileLogger();
                }
            }
        }
        return instance;
    }

    @Override
    public void logMessage(String message) {
        // 实现文件日志记录的逻辑
        System.out.println("Logging to file: " + message);
    }
}

/**
 * 数据库日志实例类
 */
 class DatabaseLogger extends Logger {
    private static DatabaseLogger instance;
    private String dbName;

    private DatabaseLogger() {
        // 私有化构造函数
        dbName = "mydb";
    }

    public static DatabaseLogger getInstance() {
        if (instance == null) {
            synchronized (DatabaseLogger.class) {
                if (instance == null) {
                    instance = new DatabaseLogger();
                }
            }
        }
        return instance;
    }

    @Override
    public void logMessage(String message) {
        // 实现数据库日志记录的逻辑
        System.out.println("Logging to database: " + message);
    }
}

/**
 * 工厂 根据type判断,并创建具体的实例类
 */
 class LoggerFactory {
    public static Logger getLogger(String loggerType) {
        if (loggerType.equalsIgnoreCase("file")) {
            return FileLogger.getInstance();
        } else if (loggerType.equalsIgnoreCase("database")) {
            return DatabaseLogger.getInstance();
        } else {
            throw new IllegalArgumentException("Invalid logger type.");
        }
    }
}

使用示例:

public class Main {
    public static void main(String[] args) {
        // 使用工厂类创建单例的日志记录器对象
        Logger fileLogger = LoggerFactory.getLogger("file");
        Logger databaseLogger = LoggerFactory.getLogger("database");

        // 记录日志
        fileLogger.logMessage("Error: File not found.");
        databaseLogger.logMessage("Warning: Database connection lost.");
    }
}

在上述示例中,我们定义了抽象类 Logger,并派生出具体的日志记录器类 FileLoggerDatabaseLogger。这两个具体类实现了抽象

6.3策略模式(Strategy Pattern)

介绍
策略模式是一种行为型设计模式,它允许在运行时根据不同的情况选择不同的算法或行为。策略模式将每个算法或行为封装成独立的类,并使它们可以互相替换,以便在不修改客户端代码的情况下改变算法或行为的选择。

在策略模式中,通常会定义一个抽象策略接口或类,表示不同的策略。然后,具体的策略类将实现该接口或继承该抽象类,并提供自己的具体实现。客户端代码将使用策略接口来执行不同的策略。

下面是一个使用策略模式的案例,假设我们有一个简单的支付系统,根据不同的支付方式选择不同的支付策略:

首先,定义一个抽象策略接口 PaymentStrategy,其中包含一个 pay 方法:

public interface PaymentStrategy {
    void pay(double amount);
}

然后,实现两种具体的支付策略类:CreditCardPaymentStrategyPayPalPaymentStrategy

public class CreditCardPaymentStrategy implements PaymentStrategy {
    private String cardNumber;
    private String expirationDate;
    private String cvv;

    public CreditCardPaymentStrategy(String cardNumber, String expirationDate, String cvv) {
        this.cardNumber = cardNumber;
        this.expirationDate = expirationDate;
        this.cvv = cvv;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paying " + amount + " dollars using credit card.");
        // 具体的支付逻辑
    }
}

public class PayPalPaymentStrategy implements PaymentStrategy {
    private String email;
    private String password;

    public PayPalPaymentStrategy(String email, String password) {
        this.email = email;
        this.password = password;
    }

    @Override
    public void pay(double amount) {
        System.out.println("Paying " + amount + " dollars using PayPal.");
        // 具体的支付逻辑
    }
}

接下来,定义一个支付上下文类 PaymentContext,它包含一个策略接口类型的成员变量,并提供设置策略和执行支付的方法:

public class PaymentContext {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void executePayment(double amount) {
        paymentStrategy.pay(amount);
    }
}

使用示例:

public class Main {
    public static void main(String[] args) {
        // 创建支付上下文
        PaymentContext paymentContext = new PaymentContext();

        // 使用信用卡支付策略
        PaymentStrategy creditCardPayment = new CreditCardPaymentStrategy("1234 5678 9012 3456", "12/25", "123");
        paymentContext.setPaymentStrategy(creditCardPayment);
        paymentContext.executePayment(100.0);

        // 使用PayPal支付策略
        PaymentStrategy payPalPayment = new PayPalPaymentStrategy("

example@example.com", "password123");
        paymentContext.setPaymentStrategy(payPalPayment);
        paymentContext.executePayment(50.0);
    }
}

在上述示例中,我们定义了两种具体的支付策略:CreditCardPaymentStrategyPayPalPaymentStrategyPaymentContext 类作为支付上下文,通过设置不同的支付策略,可以执行不同的支付操作。

Main 类的 main 方法中,我们创建了一个支付上下文对象 paymentContext,然后先使用信用卡支付策略进行支付,再使用 PayPal 支付策略进行支付。通过设置不同的支付策略,我们可以在运行时选择不同的支付方式,并执行相应的支付逻辑。

2.设计模式的使用场景

单例模式(Singleton Pattern):适用于需要确保只有一个实例存在的情况,比如数据库连接池、日志记录器等。
工厂模式(Factory Pattern):适用于需要根据参数创建不同对象的情况,比如创建不同类型的数据库连接。
抽象工厂模式(Abstract Factory Pattern):适用于需要创建一系列相关对象的情况,比如创建不同操作系统下的窗口、按钮等界面组件。
建造者模式(Builder Pattern):适用于创建复杂对象的情况,通过将对象的构建步骤进行组合和组装,使得创建过程更灵活、可扩展。
原型模式(Prototype Pattern):适用于需要创建大量相似对象的情况,通过复制已有对象来提高创建效率。
适配器模式(Adapter Pattern):适用于需要将一个类的接口转换为另一个类的情况,解决接口不兼容的问题。
装饰器模式(Decorator Pattern):适用于需要动态地为对象添加额外功能的情况,比如为文件流添加缓冲区、加密等操作。
代理模式(Proxy Pattern):适用于需要通过代理对象控制对实际对象的访问的情况,可以实现延迟加载、权限控制等功能。
观察者模式(Observer Pattern):适用于对象间存在一对多的依赖关系,当一个对象状态发生改变时,通知所有依赖它的对象。
策略模式(Strategy Pattern):适用于需要在运行时动态选择算法的情况,通过定义一系列算法并封装起来,使得可以灵活切换算法实现。
模板方法模式(Template Method Pattern):适用于定义一个算法的骨架,将一些步骤延迟到子类实现的情况。
迭代器模式(Iterator Pattern):适用于需要遍历集合对象的情况,通过提供一种统一的访问方式,可以遍历不同类型的集合对象。

7.反射

1.反射是什么

Java 程序开发语言的特征之一,可以在运行时获取一个类的所有信息,可以获取到任何定义的信息(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。

2.获取class对象的三种方式

		对象.getclass().class
		Class.forname("类全路径")

3.反射的运行原理

磁盘中的.java源码通过javac编译成.class字节码文件,classLoad类加载器将.class字节码加载到虚拟机中,文件中的成员变量封装为成员类,方法被封装为成员方法类,构造方法被封装为构造类,运行时则进入运行阶段,加载到内存中变为class类

8.异常

1.常见异常
1、算术异常类:ArithmeticException
2、空指针异常类:NullpointerException
3、类型强制转换异常:ClassCastException
4、数组下标越界异常:ArrayIndexOutOfBoundsException
5、文件未找到异常:FileNotFoundException
6、操作数据库异常:SQLException
7、I/O 异常的根类:IOException
2.异常处理
处理异常有两种方式

1.在方法声明的位置上使用throws关键字抛出,谁调用就抛给谁
2.使用try…catch语句对异常进行捕捉。这个异常不会上报,自己把异常事件处理了。异常抛到此处位置为止,不在上抛了。

3.异常的分类

1.error
Error是非程序异常,即程序不能捕获的异常,一般是编译或者系统性的错误,如OutOfMemorry内存溢出异常等
2.exception
Exception是程序异常类,由程序内部产生。Exception又分为运行时异常,非运行时异常类。

9.常用类

9.1 String

1.String常用的方法有

indexOf():返回指定字符的索引。
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
equals():字符串比较。

2.Stringbuilder / StringBuffer的区别
1.从线程安全方面来说:StringBuffer是线程安全的。StringBuilder是线程不安全的
2. 从执行效率上来说,StringBuilder > StringBuffer > String
3.源码体现:本质上都是在调用父类抽象类AbstractStringBuilder来干活,只不过Buffer把代码加了同步关键字,使得程序可以保证线程安全

9.2 Object

常用方法 toString( ), equles( ) getClass(), hashCode( )

9.3 数组

1.数组常用方法

push : 尾部追加
sort: : 排序
join : 分隔
reverse : 反转
splice :删除元素或者添加元素,

2.数组扩容原理
利用数组复制方法可以变通的实现数组扩容。
System.arraycopy()可以复制数组。
Arrays.copyOf()可以简便的创建数组副本。
创建数组副本的同时将数组长度增加就变通的实现了数组的扩容。

10 其他

Linux基础

Linux常用命令大全

常用指令
解压 tar
查看进程 ps
关闭进程 kill /kill -9
查看日志 grep tail

cookie / session的区别

存储位置不同:session 存储在服务器端;cookie 存储在浏览器端。
安全性不同:cookie 安全性一般,在浏览器存储,可以被伪造和修改。
容量和个数限制:cookie 有容量限制,每个站点下的 cookie 也有个数限制。
存储的多样性:session 可以存储在 Redis 中、数据库中、应用程序中;而 cookie 只能存储在浏览器中。

转发 、 重定向的区别

地址栏 url 显示:foward url 不会发生改变,redirect url 会发生改变;
数据共享:forward 可以共享 request 里的数据,redirect 不能共享;
效率:forward 比 redirect 效率高。

http与https 的区别

HTTP协议以明文方式发送内容,不提供任何方式的数据加密。HTTP协议不适合传输一些敏感信息
https则是具有安全性的ssl加密传输协议
http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
https协议需要到ca申请证书。HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。

你可能感兴趣的:(Java八股文专栏,java,数据结构,开发语言)