Jave基础面试总结

JavaSE

什么是OOP(面向对象)?

面向对象是java当中的一种编程思想,他的特点是继承,封装,多态,抽象

继承:提高代码重用性/可维护行(一个类派生一个新的类,子类实现了父类非私有化的方法,然后其的基础上扩展了其他的行为)

多态:提取对象之间的差异性(重写/重载)

封装:隐藏我们内部类的具体实现,对外提供公共接口(get/set方法/构造器),(对象中的方法/属性,具体的细节隐藏起来)提高了对外的安全性

抽象:提取对象之间的公共点

继承和封装的区别?

这个主要说上面的部分!!

抽象类和接口的区别?

Java提供和支持创建抽象类和接口。它们的实现有共同点,不同点在于:
接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
类可以实现很多个接口,但是只能继承一个抽象类
类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
抽象类可以在不提供接口方法实现的情况下实现接口。
Java接口中声明的变量默认都是final的(常量)。抽象类可以包含非final的变量。
Java接口中的成员函数默认是public的()。抽象类的成员函数可以是private,protected或者是public。
接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含main方法的话是可以被调用的。
也可以参考JDK8中抽象类和接口的区别

Overload(重载)和Override重写的区别

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分

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

再简单点:

重载:一个类中,编译时,同名不同参

重写:子父类,运行时,子类重写父类方法

Overload

重载Overload表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同,即参数个数或类型不同。

Override

重写Override表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。子类覆盖父类的方法时,只能比父类抛出更少的异常,或者是抛出父类抛出的异常的子异常,因为子类可以解决父类的一些问题,不能比父类有更多的问题。子类方法的访问权限只能比父类的更大,不能更小。如果父类的方法是private类型,那么,子类则不存在覆盖的限制,相当于子类中增加了一个全新的方法。

Overloaded的方法是否可以改变返回值的类型?

1.如果几个Overloaded的方法的参数列表不一样,它们的返回者类型当然也可以不一样。

2.如果两个方法的参数列表完全一样,是否可以让它们的返回值不同来实现重载Overload?答案是不行的。例如,我们调用map.remove(key)方法时,虽然remove方法有返回值,但是我们通常都不会定义接收返回结果的变量,这时候假设该类中有两个名称和参数列表完全相同的方法,仅仅是返回类型不同,java就无法确定编程者倒底是想调用哪个方法了,因为它无法通过返回结果类型来判断。

Override注意事项

Override是覆盖了一个方法并且对其重写,以求达到不同的作用。对我们来说最熟悉的覆盖就是对接口方法的实现。另外,我们在继承中也可能会在子类覆盖父类中的方法。在覆盖时要注意以下几点:

1.覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;

2.覆盖的方法的返回值必须和被覆盖的方法的返回值一致;

3.覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;

4.被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。

Overload注意事项

Overload是指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法,然后再调用时,VM就会根据不同的参数样式,来选择合适的方法执行。在重载时要注意以下几点:

1.在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是fun(int,float),但是不能为fun(int,int));

2.不能通过访问权限、返回类型、抛出的异常进行重载;

3.方法的异常类型和数目不会对重载造成影响;

4.对于继承来说,如果某一方法在父类中是访问权限是priavte,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果。

Java的基本数据类型有哪些?各占几个字节?

四类八种字节数数据表示范围
整型byte1-128~127
short2-32768~32767
int4-2147483648~2147483647
long8-263~263-1
浮点型float4-3.403E38~3.403E38
double8-1.798E308~1.798E308
字符型2表示一个字符,如(‘a’,‘A’,‘0’,‘家’)
boolean1只有两个值 true 与 false

字符串连接与哪几种方式?区别是什么?

我所知道的方式四种:+,stringBuilder,StringBuffer,spring 扩展StringUtils

+他的底层就是stringBuilder的append

使用concat()

append() (stringBuilder,StringBuffer都有)

join(StringUtils)

效率:

StringBuilder`<`StringBuffer`<`concat`<`+`<`StringUtils.join

总结:

1、如果不是在循环体中进行字符串拼接的话,直接使用+就好了。

2、如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder

stringBuilder,StringBuffer的区别

区别在于

StringBufferd支持并发操作,线性安全的,适 合多线程中使用。

StringBuilder不支持并发操作,线性不安全的,不适合多线程中使用。

新引入的StringBuilder类不是线程安全的,但其在单线程中的性能比StringBuffer高。

如何选择使用?

在单线程下运行,或者是不必考虑到线程同步问题,我们应该优先使用StringBuilder类;如果要保证线程安全,自然是StringBuffer。

(因为StringBuilder在单线程的情况下效率/速度最快)

Java异常的作用和分类

编译时:我们能处理 可以手动的进行修改

运行时:抓异常 能处理的处理,不能处理的抛异常(系统级别的异常)——最后是由(servler处理)tomcat处理——jvm处理

字节流和字符流的区别

  1. 字节流继承inputStream和OutputStream
  2. 字符流继承自InputSteamReader和OutputStreamWriter

.字节流和字符流哪个好?怎么选择?

  1. 大多数情况下使用字节流会更好,因为大多数时候 IO 操作都是直接操作磁盘文件,所以这些流在传输时都是以字节的方式进行的(图片等都是按字节存储的)(字节流也可以处理字符串)
  2. 如果对于操作需要通过 IO 在内存中频繁处理字符串的情况使用字符流会好些,因为字符流具备缓冲区,提高了性能(处理字符串的时候用字符流,因为他具备缓冲区,提高性能)

==和equals的区别

==比较的是堆里面的地址

equals比较的是值(应该也可以比较地址)

浅拷贝和深拷贝的区别?

复制一个 Java 对象
浅拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针,不复制堆内存中的对象。

img

深拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针和堆内存中的对象。

img

25 如何实现对象的克隆22

  • 实现 Cloneable 接口,重写 clone() 方法。
  • 不实现 Cloneable 接口,会报 CloneNotSupportedException 异常。

方法1、对象的属性的Class 也实现 Cloneable 接口,在克隆对象时也手动克隆属性。

方法2、结合序列化(JDK java.io.Serializable 接口、JSON格式、XML格式等),完成深拷贝

结合 java.io.Serializable 接口,完成深拷贝

具体没有看

序列化和反序列化是什么?如何实现Java序列化

1.概念

序列化:把Java对象转换为(二进制)字节序列的过程
  反序列化:把(二进制)字节序列恢复为Java对象的过程。

2.用途

对象的序列化主要有两种用途:
  1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2) 在网络上传送对象的字节序列。

  • 序列化
    以流的方式写进文件中保存,也叫写对象
  • 反序列化
    把文件中的对象以流的方式读出来,也叫读对象
  • img

使用

  • 序列化

    步骤:

      1、 创建ObjectOutPutStream对象,构造方法中传递字节输出流
      2、 使用ObjectOutPutStream对象中的方法writeObject,把对象写入到文件中
      3、 资源释放
    123
    

    代码实现:

    		// 1、 创建ObjectOutPutStream对象,构造方法中传递字节输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("day03-code\\person.txt"));
    		// 2、 使用ObjectOutPutStream对象中的方法writeObject,把对象写入到文件中
            oos.writeObject(new Person("小妹妹",80));
    		// 3、 资源释放
            oos.close();
    123456
    

    结果展现:
    在这里插入图片描述

  • 反序列化

    步骤:

      	1、 创建ObjectInPutStream对象,构造方法中传递字节输出流
      	2、 使用ObjectInPutStream对象中的方法readObject,把对象写入到文件中
      	3、 释放资源
      	4、 使用读取出来的对象(打印)
    1234
    

    代码实现:

        // 1、 创建ObjectInPutStream对象,构造方法中传递字节输出流
        ObjectInputStream ois= new ObjectInputStream(new FileInputStream("day03-code\\person.txt"));
    
        // 2、 使用ObjectInPutStream对象中的方法readObject,把对象写入到文件中
        Object o = ois.readObject();
    
        // 3、 释放资源
        ois.close();
        // 4、 使用读取出来的对象(打印)
        System.out.println(o);
    1234567891011
    

    结果展现:
    在这里插入图片描述

你知道的常见的集合有哪些?简要说一下都有什么特点和区别?

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

  • ArrayList

​ 使用数组实现

​ 有序可重复,长度可变,线程异步(先进先出)

​ 查询快,增删慢(数据少的时候,增删快)

​ 线程不安全,效率高

  • LinkedList

​ 使用链表实现

​ 有序可重复

​ 查询慢,增删快 ,长度可变,线程异步

​ 线程不安全,效率高

  • HashMap

​ 使用链表+数组 (1.8加入红黑树)

​ 键必须唯一,值可不唯一(键值均可为null)并且null元素会计入size(占用一个位置)

​ 存取速度快

​ 线程不安全

实际场景:

随机产生10万个数据,数据范围在1~1000,统计每个数据出现的次数?

  • 思路:
  1. 随机产生10万个数据:Math.random()
  2. ArrayList存放数据:要统计每个数据出现的次数,查询的效率要高,所以使用ArrayList
  3. 做统计:HashMap应用:key:存放当前数据;value:存放当前数据出现的次数
  4. 对HashMap的遍历,打印统计结果

非并发场景使用HashMap

  • HashTable

​ 使用链表+数组

​ 键必须唯一,值可不唯一(键值均不可为null)当出现null值会抛出异常

​ 存取速度较HashMap慢,但数据一致性强

线程安全!!!

并发场景可以使用Hashtable

内部是如何实现的?(画图)

  • ArrayList
  • LinkedList
  • HashMap
  • HashTable

如何对HashMap进行排序

import java.util.*;

public class HashMapSort {

    public static void main(String[] args) {
        // key=手机品牌  value=价格
        Map phone = new HashMap();
        phone.put("Apple", 7299);
        phone.put("SAMSUNG", 6000);
        phone.put("Meizu", 2698);
        phone.put("Xiaomi", 2400);
        
        //根据key进行排序(根据手机品牌)
        Set set = phone.keySet();
        Object[] arr = set.toArray();
        Arrays.sort(arr);
        //输出
        for (Object key : arr) {
            System.out.println(key + ": " + phone.get(key));
        }
        
        System.out.println("_____________________");
        //根据value进行排序(根据手机价格)
		
        //转换为List
        List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(phone.entrySet());
        
        //(第一种方式)
        //使用list.sort()排序   JDK1.8之后才出
        list.sort(new Comparator<Map.Entry<String, Integer>>() {
            @Override
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                return o2.getValue().compareTo(o1.getValue());
            }
        });
        //第二种方式
        //collections.sort()  
        Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {
            @Override
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                return o2.getValue().compareTo(o1.getValue());
            }
        });
        
        //输出
        
        //for
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i).getKey() + ": " + list.get(i).getValue());
        }
        
        System.out.println("________________");
        //for-each
        for (Map.Entry<String, Integer> mapping : list) {
            System.out.println(mapping.getKey() + ": " + mapping.getValue());
        }
    }
}

如何实现多线程编程?

  • 继承Thread类
  • 实现Runnable接口
  • 使用Executor框架的功能类

Java中的wait和sleep的区别?

sleep()

1、属于Thread类,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态

2、sleep方法没有释放锁

3、sleep必须捕获异常

4、sleep可以在 任何地方使用

wait()

1、属于Object,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程

2、wait方法释放了锁

3、wait不需要捕获异常

4、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用

synchronized、volatile、lock关键字的作用是什么?

主要相同点:Lock能完成synchronized所实现的所有功能

主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

Lock是类 其他两个都是关键字

详解:

volatile

volatile 关键字的作用是禁止指令的重排序,强制从公共堆栈中取得变量的值,而不是从线程私有的数据栈中取变量的值。
volatilesynchronized的区别如下:

  • volatile 不会发生线程阻塞,而 synchronized 会发生线程阻塞。
  • volatile 只能修饰变量,而 synchronized 可以修饰方法、代码块等。
  • volatile 不能保证原子性(不能保证线程安全),而 synchronized 可以保证原子性。
  • volatile 解决的是变量在多线程之间的可见性,而 synchronized 解决的是多线程之间访问资源的同步性。

lock

synchronized隐式锁,在需要同步的对象中加入此控制,而lock显示锁,需要显示指定起始位置和终止位置。

  • 使用lock时在finally中必须释放锁,不然容易造成线程死锁;而使用synchronized时,获取锁的线程会在执行完同步代码后释放锁(或者JVM会在线程执行发生异常时释放锁)。

  • 使用lock时线程不会一直等待;而使用synchronized时,假设A线程获得锁后阻塞,其他线程会一直等待。

  • lock可重入、可中断、可公平也可不公平;而synchronized可重入但不可中断、非公平。

    synchronized

    synchronized是Java中的关键字,是一种同步锁。有以下几种用法:

    1、修饰方法:在范围操作符之后,返回类型声明之前使用。每次只能有一个线程进入该方法,此时线程获得的是成员锁。

    public synchronized void syncMethod() {
        //doSomething
    }123
    

    2、修饰代码块:每次只能有一个线程进入该代码块,此时线程获得的是成员锁。

    public int synMethod(int arg){
        synchronized(arg) {
            //doSomething
        }
    }12345
    

    3、修饰对象:如果当前线程进入,那么其他线程在该类所有对象上的任何操作都不能进行,此时当前线程获得的是对象锁。

    public class SyncThread implements Runnable {
        public static void main(String args[]) {
            SyncThread syncThread = new SyncThread();
            Thread therad1 = new Thread(syncThread, "therad1");
            Thread therad2 = new Thread(syncThread, "therad2");
            Thread therad3 = new Thread(syncThread, "therad3");
            therad1.start();
            therad2.start();
            therad3.start();
        }
        public void run() {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName());
            }
        }
    }12345678910111213141516
    

    4、修饰类:如果当前线程进入,那么其他线程在该类中所有操作不能进行,包括静态变量和静态方法,此时当前线程获得的是对象锁。

    public class syncClass {
        public void method() {
            synchronized(syncClass.class) {
                //doSomething
            }
        }
    }1234567
    

线程池是什么?

线程池帮我们重复管理线程,避免创建大量的线程增加开销。

除了降低开销以外,线程池也可以提高响应速度,了解点 JVM 的同学可能知道,一个对象的创建大概需要经过以下几步:

  1. 检查对应的类是否已经被加载、解析和初始化
  2. 类加载后,为新生对象分配内存
  3. 将分配到的内存空间初始为 0
  4. 对对象进行关键信息的设置,比如对象的哈希码等
  5. 然后执行 init 方法初始化对象

创建一个对象的开销需要经过这么多步,也是需要时间的嘛,那可以复用已经创建好的线程的线程池,自然也在提高响应速度上做了贡献。

常见的线程池有哪些?

可以去这:http://blog.csdn.net/seu_calvin/article/details/52415337

凡事得靠ThreadPoolExecutor(铺垫环节,懂的直接跳过)

FixThreadPool(一堆人排队上公厕)

SingleThreadPool(公厕里只有一个坑位)

CachedThreadPool(一堆人去一家很大的咖啡馆喝咖啡)

ScheduledThreadPool(4个里面唯一一个有延迟执行和周期重复执行的线程池)

  • newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  • newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

  • newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

什么是死锁?如何产生?如何解决

一、什么是死锁?
如果一个进程集合里面的每个进程都在等待这个集合中的其他一个进程(包括自身)才能继续往下执行,若无外力他们将无法推进,这种情况就是死锁,处于死锁状态的进程称为死锁进程。
二、死锁产生的原因?
1.因竞争资源发生死锁现象:系统中供多个进程共享的资源的数目不足以满足全部进程的需要时,就会引起对诸资源的竞争而发生死锁现象;
(1)可剥夺资源和不可剥夺资源:可剥夺资源是指某进程在获得该类资源时,该资源同样可以被其他进程或系统剥夺,不可剥夺资源是指当系统把该类资源分配给某个进程时,不能强制收回,只能在该进程使用完成后自动释放

(2)竞争不可剥夺资源:系统中不可剥夺资源的数目不足以满足诸进程运行的要求,则发生在运行进程中,不同的进程因争夺这些资源陷入僵局。
举例说明: 资源A,B; 进程C,D
资源A,B都是不可剥夺资源:一个进程申请了之后,不能强制收回,只能进程结束之后自动释放。内存就是可剥夺资源
进程C申请了资源A,进程D申请了资源B。
接下来C的操作用到资源B,D的资源用到资源A。但是C,D都得不到接下来的资源,那么就引发了死锁。
(3)竞争临时资源
2.进程推进顺序不当发生死锁
三. 产生死锁的四个必要条件?
(1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
(2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
(3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放

(4)环路等待条件:是指进程发生死锁后,必然存在一个进程–资源之间的环形链
四. 处理死锁的基本方法
1.预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件
2.避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁
3.检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉
4.解除死锁:该方法与检测死锁配合使用

进程和线程的区别?

1、进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)

2、进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

3、线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

4、但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进行至少包括一个线程。

并发和并行的区别?

你吃饭到一半,电话来了,你一直吃完,再去接(不支持并发也不支持并行)
你吃饭到一半,电话来了,你停下来接电话,接完后继续吃饭(支持并发)
你吃饭到一半,电话来了,边吃饭,边接电话(支持并行)
并发包含并行,但是不能反过来说
并发的关键在于你有处理多个任务的能力,不一定同时(并发包含并行)
并行的关键在于你有同时处理多个任务的能力

常见的设计模式有哪些?能否举例说明(Spring)

单例模式

java中单例模式是一种常见的设计模式,单例模式的写法有好几种,主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。

单例模式有一下特点:

1、单例类只能有一个实例

2、单例类必须自己创建自己的唯一实例。

3、单例类必须给所有其他对象提供这一实例

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例

一、懒汉式单例
//懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
    private Singleton() {}
    private static Singleton single=null;
    //静态工厂方法 
    public static Singleton getInstance() {
         if (single == null) {  
             single = new Singleton();
         }  
        return single;
    }
}

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。

(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)

但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全,如果你第一次接触单例模式,对线程安全不是很了解,可以先跳过下面这三小条,去看饿汉式单例,等看完后面再回头考虑线程安全的问题:

1、在getInstance方法上加同步

public static synchronized Singleton getInstance() {
         if (single == null) {  
             single = new Singleton();
         } 
        return single;
}

2、双重检查锁定

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

3、静态内部类

public class Singleton {  
    private static class LazyHolder {  
       private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){} 
    public static final Singleton getInstance() {  
       return LazyHolder.INSTANCE;  
    }  
}  

这种比上面1、2都好一些,既实现了线程安全,又避免了同步带来的性能影响。

二、饿汉式单例
//饿汉式单例类.在类初始化时,已经自行实例化 
public class Singleton1 {
    private Singleton1() {}
    private static final Singleton1 single = new Singleton1();
    //静态工厂方法 
    public static Singleton1 getInstance() {
        return single;
    }
}

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

三、登记式单例
//类似Spring里面的方法,将类名注册,下次从里面直接获取。
public class Singleton3 {
    private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
    static{
        Singleton3 single = new Singleton3();
        map.put(single.getClass().getName(), single);
    }
    //保护的默认构造子
    protected Singleton3(){}
    //静态工厂方法,返还此类惟一的实例
    public static Singleton3 getInstance(String name) {
        if(name == null) {
            name = Singleton3.class.getName();
            System.out.println("name == null"+"--->name="+name);
        }
        if(map.get(name) == null) {
            try {
                map.put(name, (Singleton3) Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return map.get(name);
    }
    //一个示意性的商业方法
    public String about() {    
        return "Hello, I am RegSingleton.";    
    }    
    public static void main(String[] args) {
        Singleton3 single3 = Singleton3.getInstance(null);
        System.out.println(single3.about());
    }
}

登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。

这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。

饿汉式和懒汉式区别

从名字上来说,饿汉和懒汉,

饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,

而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

另外从以下两点再区分以下这两种方式:

1、线程安全:

饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。

2、资源加载和性能:

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,

而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

至于1、2、3这三种实现又有些区别,

第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,

第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗

第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

什么是线程安全?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。

​ 由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。

原型模式

定义:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。

img

原型模式主要用于对象的复制,它的核心是就是类图中的原型类Prototype。Prototype类需要具备以下两个条件:

  • 实现Cloneable接口。在java语言有一个Cloneable接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出CloneNotSupportedException异常。
  • 重写Object类中的clone方法。Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此,Prototype类需要将clone方法的作用域修改为public类型。

​ 原型模式是一种比较简单的模式,也非常容易理解,实现一个接口,重写一个方法即完成了原型模式。在实际应用中,原型模式很少单独出现。经常与其他模式混用,他的原型类Prototype也常用抽象类来替代。

工厂模式

当每个抽象产品都有多于一个的具体子类的时候(空调有型号A和B两种,发动机也有型号A和B两种),工厂角色怎么知道实例化哪一个子类呢?比如每个抽象产品角色都有两个具体产品(产品空调有两个具体产品空调A和空调B)。抽象工厂模式提供两个具体工厂角色(宝马320系列工厂和宝马230系列工厂),分别对应于这两个具体产品角色,每一个具体工厂角色只负责某一个产品角色的实例化。每一个具体工厂类只负责创建抽象产品的某一个具体子类的实例。

代理模式

对一些对象提供代理,以限制那些对象去访问其它对象。

代理模式主要使用了java的多态,干活的是被代理类,代理类主要是接活,你让我干活,好,我交给幕后的类去干,你满意就成,那怎么知道被代理类能不能干呢?同根就成,大家知根知底,你能做啥,我能做啥都清楚得很,同样一个接口。

适配器模式

将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。

解决的问题

即Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。

  1. 模式中的角色

3.1 目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。

3.2 需要适配的类(Adaptee):需要适配的类或适配者类。

3.3 适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口。

  1. 实现方式

(1)类的适配器模式(采用继承实现)

(2)对象适配器(采用对象组合方式实现)

img

模板方法模式

概述

模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。

比如定义一个操作中的算法的骨架,将步骤延迟到子类中。模板方法使得子类能够不去改变一个算法的结构即可重定义算法的某些特定步骤。

模式中的角色

抽象类(AbstractClass):实现了模板方法,定义了算法的骨架。

具体类(ConcreteClass):实现抽象类中的抽象方法,已完成完整的算法。

应用

举个例子,以准备去学校所要做的工作(prepareGotoSchool)为例,假设需要分三步:穿衣服(dressUp),吃早饭(eatBreakfast),带上东西(takeThings)。学生和老师要做得具体事情肯定有所区别。

优点

模板方法模式通过把不变的行为搬移到超类,去除了子类中的重复代码。

子类实现算法的某些细节,有助于算法的扩展。

通过一个父类调用子类实现的操作,通过子类扩展增加新的行为,符合“开放-封闭原则”。

缺点

每个不同的实现都需要定义一个子类,这会导致类的个数的增加,设计更加抽象。

适用场景

在某些类的算法中,用了相同的方法,造成代码的重复。

控制子类扩展,子类必须遵守算法规则。

过滤器模式

过滤器模式是一种设计模式,使开发人员可以使用不同的条件过滤一组对象,并通过逻辑操作以解耦方式将其链接。 这种类型的设计模式属于结构模式,因为该模式组合多个标准以获得单个标准。

拦截器模式

拦截器模式:当软件系统或者框架希望提供一种方式,能够改变或者增强平常的处理流程。

​ 比如,典型的场合是在web服务器中的应用。当web服务器接受到一个从浏览器传送过来的URL请求时,web服务器将该URL映射到某一个文件、打开该文件、并将文件内容回传给浏览器。上面是普通的一个处理流程,应用拦截器模式后,上面的任何一个步骤都可以被替换或者更改,比如,将URL映射到一个新的文件名,又或者插入一个新的步骤,比如加上处理文件的内容之后,再回传给浏览器。

​ 拦截器模式的关键点在于:改变是透明的以及自动被使用的,也就是说,系统的剩余部分都不需要知道某些东西已经有所改变,而是与之前一样继续运行。那么,需要实现预先设计好的某些接口、实现拦截器注册用的某些调度机制(动态的、运行时的或者静态的,例如,可以在配置文件中实现)以及提供“上下文对象(context object)”(可以从中获取到框架的内部状态)

​ 典型应用:

​ web服务器、面向对象和面向消息的中间件。

​ 详细的有Java中的javax.servlet.Filter则实现了该模式。

MVC模式

MVC是一种架构型模式,它本身并不引入新的功能,只是用来指导我们改善应用程序的架构,使得应用的模型和视图相分离,从而得到更好的开发和维护效率。
在MVC模式中,应用程序被划分成了模型(Model)、视图(View)和控制器(Controller)三个部分。其中,模型部分包含了应用程序的业务逻辑和业务数据;视图部分封装了应用程序的输出形式,也就是通常所说的页面或者是界面;而控制器部分负责协调模型和视图,根据用户请求来选择要调用哪个模型来处理业务,以及最终由哪个视图为用户做出应答。
MVC模式的这三个部分的职责非常明确,而且相互分离,因此每个部分都可以独立的改变而不影响其他部分,从而大大提高了应用的灵活性和重用性

前端控制器模式

介绍

前端控制器模式(Front Controller Pattern)是用来提供一个集中的请求处理机制,所有的请求都将由一个单一的处理程序处理。该处理程序可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。以下是这种设计模式的实体。

  • 前端控制器(Front Controller) - 处理应用程序所有类型请求的单个处理程序,应用程序可以是基于 web 的应用程序,也可以是基于桌面的应用程序。
  • 调度器(Dispatcher) - 前端控制器可能使用一个调度器对象来调度请求到相应的具体处理程序。
  • 视图(View) - 视图是为请求而创建的对象。
实现

我们将创建 FrontControllerDispatcher 分别当作前端控制器和调度器。HomeViewStudentView 表示各种为前端控制器接收到的请求而创建的视图。

谈谈JVM的内存结构和内存分配

Java内存模型

Java虚拟机将其管辖的内存大致分为三个逻辑部分:
方法区(Method Area),Java栈(Stack),Java堆(heap)
1. 方法区是静态分配的,编译器将变量绑定在某个存储位置上,而且这些绑定不会在运行时改变。常数池,源代码中的命名常量,String常量和static变量保存在方法区。
2. Java Stack 是一个逻辑概念,特点是后进先出。一个栈的空间可能是连续的,也可能是不连续的。最典型的Stack应用是方法的调用,Java虚拟机每调用一次方法就创建一个方法帧(frame),退出该方法对应的方法帧(frame)被弹出(pop)。栈中存储的数据也是运行时确定的。最典型的Stack应用是方法的调用,Java虚拟机每调用一次方法就创建一个方法帧(frame),退出该方法对应的方法帧(frame)被弹出(pop)。栈中存储的数据也是运行时确定的。
4. Java堆分配(heap allocation)意味着以随意的顺序,在运行时进行存储空间分配和收回内存管理模型。堆中存储的数据常常是大小,数量和生命期在编译时无法确定的。Java对象的内存总是在heap中分配。
1234

Java内存分配

1. 基本数据类型直接在栈空间分配;
2. 方法的形式参数,需要在栈空间分配,当方法调用完成后从栈空间回收;
3. 引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量;
4. 方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象去,当方法调用完后从栈空间回收;
5. 局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量声明周期结束后,栈空间立刻被回收,堆空间区域等待GC回收;
6. 方法调用时传入实际参数,先在栈空间分配,在方法调用完成后从占空间释放;
7. 字符串常量在DATA(存放全局变量,静态变量,字符串常量,不释放)区域分配,this在堆空间分配;
8. 数组既在栈空间分配数组名称,走在堆空间分配数组实际大小。

Heap堆和Stack栈的区别?

主要看下面的

说一下栈、堆、方法区的用法?

堆区:
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身.
3.一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。

栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
4.由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.

静态区/方法区:
1.方法区又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

3.—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。

自己总结

对象实例,数组(所有new出来的对象)全局变量放在堆里面

方法(变量)对象的引用,基本数据类型,引用(先进后出)(私有的)

  • 方法区

常量,类的信息,静态变量static,.class文件

类加载器是做什么用的?类在什么时候会被初始化?

Java类加载体系中的双亲委托机制是什么?

在加载Class对象时。类加载器首先会去尝试父级类夹杂器中加载,如果父级类加载器反馈无法加载这个类,类加载器才会尝试自己去加载。类加载器都会有自己的父级,在尝试让父级类加载器加载这个操作是一个递归的过程,一直会找到扩展类加载器这一级,然后扩展类加载器会找到启动类加载器尝试加载。可以通过ClassLoder这个类的loadClass这个函数来了解这个加载过程

以上不是我自己总结的!!!!

说一下Java的GC?

  • GC是什么?

——GC是垃圾收集的意思

  • 为什么会有GC机制?

——内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃 为了解决这个问题,所以出现了GC机制

  • 哪些内存需要回收

——主要针对方法区的动态内存分配和回收

  • GC机制和算法(策略)

——不同的区不同的算法1.7和1.8 的算法也是不一样的!!

常用的垃圾回收算法(具体是什么意思再去百度)

  1. 引用计数算法
  2. 标记-清除算法
  3. 复制算法
  4. 标记-整理算法
  5. 分代收集算法
  • 什么时候GC回收

——1,Allocation Failure:堆内存分配不足导致gc回收。

——2,System.gc():开发者调用API请求gc回收。

你在开发过程中遇到过内存溢出么?什么原因导致的?解决方法有哪些?

引起内存溢出的原因有很多种,常见的有以下几种:

1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3.代码中存在死循环或循环产生过多重复的对象实体;
  4.使用的第三方软件中的BUG;
  5.启动参数内存值设定的过小;

内存溢出的解决方案

第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

重点排查以下几点:
  1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
  2.检查代码中是否有死循环或递归调用。

3.检查是否有大循环重复产生新对象实体。

4.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中 数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。

5.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

第四步,使用内存查看工具动态查看内存使用情况

JDK8的新特有哪些?

  • LocalDate系列
  • Optional
  • Lambda表达式
  • 接口
接口的扩展方法

​ 在JDK1.8之前,接口中只允许有抽象方法,但是在1.8之后,接口中允许有一个非抽象的方法,但是必须使用default进行修饰,叫做扩展方法

函数式接口,方法与构造方法的引用

​ 函数式接口:接口中只有一个方法,可以对应一个lambda表达式。通过匿名内部类或者方法传递进行连接,调用接口即调用对应的方法。

时间表达式(LocalDate系列)这个容易懂
LocalDate today = LocalDate.now(); //现在的日期
LocalDate tomorrow = today.plusDays(1); //今天之后的一天的日期
LocalTime time = LocalTime.of(10,10,10,358); //设定时间
LocalTime now = LocalTime.now(); //时间
LocalDateTime atDate = now.atDate(today);  //日期+时间
LocalDateTime dateTime = LocalDateTime.now(); //当前日期和时间

实际使用中,计算日期就用LocalDate,计算日期加时刻用LocalDateTime,如果只有时刻就是LocalTime

为什么选择:

Date:Date如果不格式化,打印出的日期可读性差!!

LocalDateTime:Date有的我都有,Date没有的我也有,日期选择请Pick Me

lambda表达式的用法
创建匿名内部类

​ lambda表达式和Stream接口对集合的操作《重点》

构建流的几种方法

常用方法

1、foreach方法

public static void main(String[] args) {

        String[] arrs = {"hello","world","welcome","meet","you""aaa"};
        List<String> list = Arrays.asList(arrs);
        test1(list);
    }

    public static void test1(List list){
        list.stream().forEach(x -> System.out.println(x)); //拿到每个元素打印
        list.stream().forEach(System.out::println);  //如果只进行打印,可以使用方法的引用。
    }

2、对数据进行过滤

list.stream().filter((s) -> s.startsWith("a")).forEach(System.out::println);  //aaa,过滤开始元素为a的字符串。

3、对数据进行排序

list.stream().filter((user) -> user.getName().startsWith("a")).sorted().forEach(System.out::println); 
 //需要特别注意的是:你对user进行排序,那么user类必须实现了Comparable<User>接口,并重写compareTo方法,来定义比较方法才可以,否则会报无法转换为Compare错误!
public class User implements Comparable<User>,Serializable {
  private Integer age;
  getter,setter省略。。。
  @Override
  public int compareTo(User user) {
      return user.getAge() - this.getAge() ;
  }
}
//实现了Comparable接口之后,系统会知道如何进行排序比较。

需要注意的是:这里排序之后只是流中进行了排序,如果想要得到排序之后的集合,需要对流进行toArray操作,然后重新转换成集合。

4、map操作

返回由给定函数应用于此流的结果组成的流。也就是我定义一个方法,对流中的每个元素进行操作。如果需要返回修改之后的list,还需要toArray

list.stream().map((user -> {
            user.setName(user.getName().toUpperCase());
            return user;
        })).forEach(System.out::println);  //定义了一个Function<User,User>接口的匿名内部类,使用lambda表达式来实现。
 //写的更加明显一点如下:
 
Function<User,User> function = (user -> {
    user.setName(user.getName().toUpperCase());
    return user;
});  //Function接口的匿名内部类
list.stream().map(function).forEach(System.out::println);

5、count操作:最终操作

long count = list.stream().filter((user) -> user.getName().startsWith("a")).count();
System.out.println("所有以a开头用户名字个数为: " + count);  // 所有以a开头用户名字个数为: 5

6、reduce操作

Stream<String> stream1 = Stream.of("aa", "bb", "cc"); //直接使用静态方法,获取指定值的顺序排序流
stream1.reduce((s1,s2) ->s1 +"#" +s2).ifPresent(System.out::println);  //aa#bb#cc

操作的目的是:允许通过指定的函数来将stream中的多个元素规约为一个元素,最后得到的是一个Optional接口容器表示,如果想拿到值,需要get()来拿到。

7、关于Stream的串行和并行

list.stream()-->即进行串行操作,单线程。时间长
list.parallelStream()-->并行操作,多线程同时进行。需要时间短
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值