Java基础知识点总结

455 阅读16分钟

String常量池问题

  1. String s = "abc"; 如果常量池中有"abc",就使用常量池中的对象,否则在常量池中新建"abc",然后使用常量池中的对象;
  2. String s = new String("abc");无论常量池有没有"abc",都会在堆中创建一个对象,对象存储的数据是"abc",如果常量池中没有"abc",那么会在常量池中创建"abc"对象;
  3. String s = "a" + "bc"; 有连接符的全是常量的运算同情况1;
  4. String x = "a"; String y = "b"; String s = x + y; 无论常量池中有没有"abc"像这种有连接符的有一个或多个变量的运算都会在堆中新建一个对象,对象存储的数据是"abc",不会在常量池中创建;
  5. intern,某个对象调用intern,如果常量池中有这个该对象的值,那么结果为常量池中那个值的地址,否则结果为调用对象的地址。

IO流

img

常用字节流:FileInputStream,FileOutputStream,BufferedInputStream,BufferedOutputStream
常用字符流:FileReader,FileWriter,BufferedReader,BufferedWriter

读一些二进制文件时使用字节流,读字符串文件时使用字符流。

缓冲:使数据不直接写入到流中,而是先写入buffer然后再一次性flush到流中。可以减少磁盘IO的次数。

Java反射

// 获取这个类的Class实例,还可使用 类.class 和 对象.getClass() 获取
Class<?> clazz = Class.forName("com.zwh.reflect.Controller");
// 获取构造方法
Constructor<?> constructor = clazz.getConstructor();
// 获取反射类对象,如果有默认的构造方法,直接clazz.newInstance();即可获取
Object reflectObject = constructor.newInstance();
// 获取方法的Method对象,没有参数就只需填写第一个参数
Method printHello = clazz.getMethod("printHelloAddSth", String.class);
// 利用invoke方法调用方法,没有参数只需填写第一个参数
printHello.invoke(reflectObject, "zwh");
// 获取属性Field对象,如果属性不是private直接getField()即可
Field service = clazz.getDeclaredField("service");
service.setAccessible(true);
// 给对象赋值
Service serviceObj = new Service();
service.set(reflectObject, serviceObj);

通过Class.forName获取Class实例,Class实例通过getConstructor()获取Constructor对象,Constructor对象通过调用newInstance()生成类对象;Class实例通过getMethod()获取Method对象,Method对象通过invoke方法调用类的方法,Class实例通过getField()获取类的成员变量。

动态代理

动态代理与静态代理各自适用的场景:业务代码内,当需要增强的业务逻辑非常通用(如:添加log,重试,统一权限判断等)时,使用动态代理将会非常简单,如果每个方法增强逻辑不同,那么静态代理更加适合。

jdk自带的Proxy

基本概念:一个面向接口的动态代理,代理一个对象去增强某个接口中的方法,没有接口不可用,只能读取接口上的注解。

/**
 * 卖酒商店需要有的方法
 */
public interface SellWine {
    /**
     * 卖酒
     */
    void maiJiu();

    /**
     * 进酒
     */
    void jinJiu();
}
public class MaoTai implements SellWine {
    @Override
    public void maiJiu() {
        System.out.println("卖茅台酒一瓶");
    }

    @Override
    public void jinJiu() {
        System.out.println("进茅台酒一箱");
    }
}

实现方式:实现基于Proxy的动态代理,既然要代理,那就要有被代理的对象,用Object声明一个成员变量,表示将要被代理的对象。其次要实现JDK里InvocationHandle接口的invoke方法(该方法有三个个参数,Object proxy,即代理对象,Method method,通过反射获取的被代理对象Method对象,Object[] args,被代理对象某个方法的参数)。在method调用invoke方法前后可以添加一些要处理的事情。此时一个代理类就创建完毕。

public class GuiTaiAProxy implements InvocationHandler {
    private Object object;

    GuiTaiAProxy(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 前置增强
        System.out.println("今日活动满100减5元");
        // 调用被代理对象的方法
        Object invoke = method.invoke(object, args);
        // 后置增强
        System.out.println("GuiTai柜台A送您一张优惠券");
        return invoke;
    }
}

使用方式:首先你得有一个需要被代理的对象A,接着创建代理对象,去代理对象A。接着需要指定哪个接口的方法需要被代理了,即使用Proxy的静态方法newProxyInstance来创建代理对象(该方法有三个参数,ClassLoader loader, 即类加载器,传入需要被代理的类加载器;Class<?>[] interfaces,需要被代理的接口;InvocationHander h 一个InvocationHandler对象,也就是实现了InvacationHandler的代理类对象)。

public static void main(String[] args) {
    // 首先你得有一个需要被代理的对象
    MaoTai maoTai = new MaoTai();
    // 接着创建代理对象,去代理上面的对象
    GuiTaiAProxy guiTaiAProxy = new GuiTaiAProxy(maoTai);
    // 接着需要指定哪个接口的方法需要被代理了
    SellWine maoTaiPlus = (SellWine) Proxy.newProxyInstance(MaoTai.class.getClassLoader(), MaoTai.class.getInterfaces(), guiTaiAProxy);
    maoTaiPlus.maiJiu();
}

Mybatis就是基于它实现的。

CGlib

面向父类的动态代理,有没有接口都可以使用,可以读取类上的注解。

AOP结合了Proxy 和 CGlib。

实现基于CGlib的动态代理,和Proxy大同小异,他是要实现MethodInterceptor接口中的intercept方法,该方法比InvocationHandle中的invoke多了一个MethodProxy proxy参数,用于调用业务类中的方法。代理对象是用Enhancer的静态方法create来创建,该方法有两个参数,第一个参数指定要代理的业务类,第二个参数指定实现了MethodInterceptor接口的类的对象。

为什么要重写equals 要重写 hashCode

因为equal结果为true的,hashcode应该是一样的才对,而系统自定义的hashcode会让一个类的不同对象的hashcode都不一样,所以从逻辑上,要重写hashcode,让equal为true的hashcode一定要一样。

还有就是hashmap的应用。假设HashMap的键是一个引用类型,那么它在存一个对象的时候,首先是根据键的hashCode方法来计算应该存在哪的,如果计算出来的结果有重复,那么就形成链表,在找的时候先根据hashCode找的链表的头结点,然后遍历链表用equals找到相应的键。

java集合类

  • ArrayList,底层数据结构是数组,有序,可重复。
  • LinkedList,底层数据结构是链表,有序,可重复。它还实现了Queue接口,可以当队列使用。
  • Vector,底层数据结构是数组,有序,可重复。
  • HashSet,底层数据结构是哈希表,无序,不可重复。----> CopyOnWriteArraySet
  • TreeSet,底层数据结构是红黑树,有序,不可重复。----> ConcurrentSkipListSet
  • LinkedHashSet,底层数据结构是链表+哈希表,有序,不可重复。
  • HashMap,底层数据结构是哈希表,无序,键不可重复。
  • TreeMap,底层数据结构是红黑树,有序,键不可重复。----> ConcurrentSkipListMap
  • HashTable,底层数据结构是哈希表,无序,键不可重复。

其中数据结构是红黑树的有序都是基于元素的自然排序或者通过比较器确定的顺序,其余的有序都是按照插入顺序来的。ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap,这些都是适合单线程使用的,他们是线程不安全的。线程安全的集合类有Vector、HashTable,以及JUC中的一些高性能集合类CopyOnWriteArrayList、ConcurrentHashMap、ConcurrentSkipListMap。

  • CopyOnWriteArrayList通过ReentrantLock对add()加锁,在有写操作的时候会copy一份数据,然后写完再设置成新的数据。保证修改时不影响其他线程的读操作。
  • ConcurrentSkipListSet称之为跳表,采用乐观锁(例如在AC中插入B,将B的指针指向C,在将A的指针指向B的时候判断A是不是还指向C),它是一种可以替代平衡树的数据结构,其数据元素默认按照key值升序,天然有序。一个节点有多个nextNode。
  • ConcurrentHashMap在jdk1.7使用的分段锁,1.8只会锁定当前链表或红黑二叉树的首节点。

线程安全与线程不安全

线程安全就是多线程访问数据时,采用了加锁机制,当一个线程访问该类的某个数据时,其他线程不能进行访问。直到该线程将锁释放,其他线程再去争抢锁。这样就不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,当多个线程先后更改数据,所得到的数据可能会是脏数据。

ArrayList

ArrayList为什么是线程不安全的:因为在并发写的时候可能会造成数据覆盖,数组越界,最终结果与预期不一致。

首先来看一下ArrayList中add()方法

public boolean add(E e) {
    // 根据增加元素后的数组大小来判断是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

其中minCapacity是添加元素后数据的长度,calculateCapacity中判断是否是空数组(如果没有给定初始化的大小,即为空数组),如果是空数组,那么数组大小为默认的10

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// elementData是原数组,minCapacity是添加元素后数组的大小
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

如果添加元素后的数据长度大于现在的长度,那么就进行扩容

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
  • 并发写之数据覆盖 在该行代码执行时elementData[size++] = e;先将e的值赋给elementData[size],然后再将size加1,如果一个线程执行了第一步以后,第二个线程也去执行第一步,那么第一个数据将会被覆盖。

  • 并发写之数组越界 在执行ensureCapacityInternal(size + 1);的时候,如果当前数组容量为10,现在已经有9个元素了,然后两个线程需要添加元素,第一个元素执行完ensureCapacityInternal(size + 1);不需要扩容,第一个线程执行完第二个线程立马执行ensureCapacityInternal(size + 1);那么也是不需要扩容的,那么他们两个线程必然会有一个在执行elementData[size++] = e;的时候会出现数据越界。

解决方案:

  1. Vector:通过在add方法上添加synchronized。
  2. CopyOnWriteArrayList:通过ReentrantLock来给add方法加锁,并使用了写时复制的机制。它与Vector对比,优点就是可以多个读操作同时进行,缺点就是,需要牺牲掉一些内存空间来copy一份数据。

关于并发读写发生异常,我觉得是另一个问题。这会在你使用Iterater的时候发生错误,如下:

public class UnsafeList {
    public static void main(String[] args) {
        List<String> list=new ArrayList<>();

        System.out.println(list);
        for(int i=0;i<10;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    list.add(UUID.randomUUID().toString());
                    System.out.println(list);
                }
            },String.valueOf(i)).start();
        }
    }
}

上面的代码在System.out.println(list);这一行为报错ConcurrentModificationException。原因是迭代器读next元素的时候,会检查modcount和最开始的modcount是不是一样的,相当于一个乐观锁。

HashMap

HashMap允许一个null键和多个null值。

HashMap线程不安全的原因是:会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题。

源码分析: 具体详见(4条消息) 面试必备:HashMap源码解析(JDK8)_zxt0601的博客-CSDN博客_hashmap源码分析

  1. HashMap的容量达到threshold域值时,就会触发扩容。扩容前后,哈希桶的长度一定会是2的次方。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量。当链表数量>=8,链表会被转化为红黑树。

  2. threshold为初始化容量 乘以 0.75(默认的加载因子),初始化容量是大于等于它的最小的一个 2^n,例如初始化容量3,则会被初始化为4。

  3. 构造函数

    //最大容量 2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    //哈希桶,存放链表。 长度是2的N次方,或者初始化时为0.
    transient Node<K,V>[] table;
    
    //加载因子,用于计算哈希表元素数量的阈值。  threshold = 哈希桶.length * loadFactor;
    final float loadFactor;
    //哈希表内元素数量的阈值,当哈希表内元素数量超过阈值时,会发生扩容resize()。
    int threshold;
    

map的遍历

// 通过keySet,二次遍历,效率低
for (String key : map.keySet()) {
    System.out.println(key + ", " + map.get(key));
}
// 通过keySet()遍历,效率高
for (Map.Entry<String, String> item : map.entrySet()) {
    System.out.println(item.getKey() + ", " + item.getValue());
}
// jdk1.8 通过forEach更加便捷,效率高
map.forEach((k, v) -> System.out.println(k + ", " + v));

map之间的相互转换 之 万恶公式

Map<String, Object> srcMap = new HashMap<>(2);
Map tempMap = srcMap;
Map<String, String> destMap = tempMap;

Java队列

阻塞队列:在队列为空时,获取元素的线程会等待队列变为非空,对应的方法是take()。当队列满时,存储元素的线程会等待队列可用,对应的方法是put(E e)。

  • 没有实现的阻塞接口的LinkedList
  • 实现了阻塞接口的
    • ArrayBlockingQueue :一个由数组支持的有界队列。
    • LinkedBlockingQueue :一个由链接节点支持的可选有界队列。
    • PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。
    • DelayQueue :一个由优先级堆支持的、基于时间的调度队列。
    • SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。 其中LinkedBlockingQueue内部读写(插入获取)各有一个锁,而ArrayBlockingQueue则读写共享一个锁。

Lambda

可以访问外部变量,但是不能对外部变量进行修改,但是可以对外部变量中的变量进行修改。

Stream接口,Stream 操作分为中间操作或者最终操作两种,而中间操作返回Stream本身,最终操作返回一特定类型的计算结果或遍历Stream本身

通过stream(), parallelStream()创建一个Stream

filter过滤 中间操作

sort排序 中间操作

map映射 中间操作

allMatch, anyMatch 最终操作,返回true,false

count 计数

forEach最终操作 对Stream中的元素依次执行

stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println);
stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println);

封装,继承,多态,抽象

封装:将同一类事物的特性与功能包装在一起,对外暴露调用的接口。隐藏实现的细节,公开使用方式。提高程序的安全性。

继承:继承是从已有的类中派生出新的类,新的类能吸收已有类的属性和行为,并能扩展新的能力。提高代码复用性。注意:一个父类是无法强转为子类的,除非该父类对象是一个上转型对象。

多态:同一个类的不同子类对象对同一个方法的调用产生不同的结果叫多态。方便程序的扩展。

抽象:通过特定的实例抽取出共同的特征行成概念的过程。

Object的方法

  • toString 定义一个对象的字符串表现形式
  • hashCode 定义一个对象的hashcode
  • equals 判断两个对象是否相同
  • clone 深拷贝,需要子类实现Cloneable接口,并重写clone方法,重写的方法中调用父类的clone方法即可
  • finalized

以下三个方法都必须在synchronized 同步关键字所限定的作用域中调用

  • wait 让一个线程释放锁,进入等待
  • notify 释放当前锁,唤醒某个竞争该锁的线程,先等待的先唤醒
  • notifyAll 释放当前锁,唤醒所有竞争该锁的线程

equals和==的区别是什么?

基本数据类型的==,是比较数值

引用类型的==,是比较内存地址

equals是Object中的方法,默认实现方式是==,通过改写equals可以自定义比较对象的方式。String重写equals方法比较的就是两个字符串的值是否相等,所以在比较两个字符串是否相等的时候要使用equals而不是==。

Array和List的相互转换

Array转List:Arrays.asList()方法返回的对象是Arrays的内部类,对list的操作仍然反映在原数组上,这个list是定长的,不支持add、remove操作;如果想支持add、remove操作使用遍历Array的方式将元素添加到List中

List转Array:

    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

Java异常

Error:是程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。

Exception:程序本身可以捕获并且可以处理的异常。

  • 运行时异常(不受检异常):RuntimeException类及其子类表示JVM在运行期间可能出现的错误。例如NullPointerException,IndexOutOfBoundsException,ClassCastException
  • 非运行时异常(受检异常):Exception中除RuntimeException极其子类之外的异常。编译器会检查此类异常,如果程序中出现此类异常,比如说IOException,FileNotFoundException,必须对该异常进行处理,要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。

Java枚举

public enum Color {  
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);  
    // 成员变量  
    private String name;  
    private int index;  
    // 构造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    // 普通方法
    public static String getName(int index) {  
        for (Color c : Color.values()) {  
            if (c.getIndex() == index) {  
                return c.name;  
            }  
        }  
        return null;  
    }  
    // get set 方法  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public int getIndex() {  
        return index;  
    }  
    public void setIndex(int index) {  
        this.index = index;  
    }  
}  

枚举类中的每一个枚举成员都可以看作是 Enum 类的实例,这些枚举成员默认都被 final、public, static 修饰,当使用枚举类型成员时,直接使用枚举名称即可调用成员。

所有枚举实例都可以调用 Enum 类的方法,常用方法如下:

  • values():以数组形式返回枚举类中的所有成员
  • ordinal():获取枚举成员的索引位置,0,1,2...
  • compareTo():比较两个枚举成员在定义时的顺序
  • valueof():将普通字符串转换为枚举实例,区分大小写

上面代码中以逗号隔开的,最后以分号结尾的,这部分叫做枚举的实例。也可以理解为,class new 出来的实例对象。这下就好理解了。

只是class new对象,可以自己随便new,想几个就几个,而这个enum关键字,他就不行,他的实例对象,只能在这个enum里面体现。

也就是说,他对应的实例是有限的。这也就是枚举的好处了,限制了某些东西的范围,举个栗子:一年四季,只能有春夏秋冬,你要是字符串表示的话,那就海了去了,但是要用枚举类型的话,你在enum的大括号里面把所有的选项,全列出来,那么这个季节的属性,对应的值,只能在里面挑,不能有其他的。

Java序列化

  1. 什么是序列化:Java中的序列化是把Java对象转换为字节序列的过程,Java中的序列化机制能够将一个实例对象的状态信息写入到一个字节流中,使其可以通过socket进行传输、或者持久化到存储数据库或文件系统中,然后在需要的时候通过字节流中的信息来重构一个相同的对象。
  2. 序列化 serialVersionUID 的作用:在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。

String concat 和 + 区别

  • concat:将指定字符串连接到此字符串的结尾。
  • +:可以把任何类型的数据连接起来

对String对象进行遍历

String s = "";
// 快
for(int i = 0, n = s.length() ; i < n ; i++) { 
    char c = s.charAt(i); 
}

// 可读性高
for(char c : s.toCharArray()) {
    // process c
}

String,数组,集合的长度获取

  • String:s.length()
  • 数组:arr.length
  • 集合:list.size() String和集合都是通过方法获取,数组是通过自己的final成员变量直接获取。

JackSon 的使用

JSON.parseObject(JSON.toJSONString(user01), User.class);