JAVA基础

目录

一、Java基础的知识点概要

1.Java基础知识点概要

二、重要知识点

1.异常

异常分类

Exception 和 Error

实战技巧

2.集合

集合框架图

fail-fast机制和fail-safe机制

List

Set

Map

HashMap

ConcurrentHashMap

TreeMap

3.反射机制

反射是什么

反射的原理

获取反射入口(class对象)的三种方法

通过反射来生成对象的两种方法

反射的用途或场景

反射的缺点

4.动态代理

代理模式

JDK动态代理

CGLIB动态代理

5.序列化

什么是序列化与反序列化

为什么要序列化与反序列化

序列化方法



一、Java基础的知识点概要

1.Java基础知识点概要

数据类型
运算符
分支结构
循环结构
面向对象(封装、继承、多态)
抽象类、接口
修饰符(static、final、public。。。)
内部类
泛型
异常
集合
反射
动态代理
序列化

二、重要知识点

1.异常

异常分类

      

Exception 和 Error

       Exception 和 Error 都是继承了 Throwable 类。
       Exception 应该被捕获,进行相应处理。Exception 又分为可检查(checked)异常和不检查(unchecked)异常。可检查异常(IOException、SQLException)在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是RuntimeException,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获。
       Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。尽量不要捕获。
       附加知识点,NoClassDefFoundError和ClassNotFoundException区别:NoClassDefFoundError 是个Error,是指一个class在编译时存在,在运行时找不到了class文件了;ClassNotFoundException 是个Exception,是使用类似Class.foName()等方法时的checked exception。

实战技巧

       1.尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常
       2.不要e.printStackTrace();而是logger.info(e.getMessage())
       3.建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码(因为try-catch 代码段会产生额外的性能开销)

2.集合

集合框架图

       Collection接口的子接口包括:Set、List、Queue
              Set的实现类:HashSet、TreeSet、LinkedHashSet
              List的实现类:ArrayList、LinkedList、Stack、Vector
              Queue的实现类:ArrayBlockingQueue、LinkedBlockingQueue
       Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap、Properties

      

fail-fast机制和fail-safe机制

       fail-fast(快速失败):当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出ConcurrentModificationException异常,java.util下都是快速失败。不能在多线程下发生并发修改(迭代过程中被修改,快速失败和安全失败是对迭代器而言的)。
       fail-safe(安全失败):在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在java.util.concurrent下都是安全失败。

List

       Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择。扩容时会提高 1 倍
       ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的。 ArrayList 扩容时增加 50%适合查询、修改
              ArrayList为什么线程不安全:
                     1.多线程进行add操作时可能会导致elementData数组越界;
                     2.elementData[size++] = e中size++不是原子操作。
              如需要换用CopyOnWriteArrayList或者Collections.synchronizedList(list)。
       LinkedList双向链表,所以它不需要扩容,它也不是线程安全的适合插入、删除

Set

       TreeSet 支持有序访问,但add、delete、contains相对低效(O(log(n)))。所有方法都是基于TreeMap实现的,底层是红黑树。与HashSet不同的是其不需要重写hashCode()和equals()方法。
       HashSet 利用哈希算法,如果哈希散列正常,可以提供时间复杂度O(1)的add、delete、contains操作。
       LinkedHashSet 内部构建了一个记录插入顺序的双向链表,也可以提供时间复杂度O(1)的add、delete、contains操作。

Map

       Hashtable早期 Java 类库提供的一个哈希表实现,本身是线程安全的(所有方法用synchronized修饰),不支持 null 键和null值。初始容量为11,扩容capacity*2+1。
       HashMap线程不安全,支持 null 键和null值。HashMap 进行 put 或者 get 操作,时间复杂度O(1)~O(n)
       TreeMap基于红黑树的一种提供顺序访问的 Map,它的get、put、remove 之类操作都是 O(logn)的时间复杂度,默认是按键值的升序排序(自然顺序),也可以指定排序的比较器Comparator。不支持 null 键和但支持null值。
       LinkedHashMap,散列表+双向链表,散列表部分和HashMap一致,而双向链表部分则是来一个就插到尾部,这样就保证了保持插入顺序。继承了HashMap类,大部分方法直接沿用,但put方法虽然未复写,put方法里有一个方法是构造一个新节点newNode,这里LinkedHashMap重写了。
       LinkedHashMap有一个构造方法可以传递accessOrder=true,可以按照访问顺序排序,最近常使用的排到最后,最近不常使用的排到最前,size()撑爆时删除最近不常使用的,实现LRU算法。利用LinkedHashMap实现LRU算法的代码如下:

Map<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(0, 0.75f, true) {
  @Override
  protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
      return size() > maxSize;
  }
};

HashMap

       数组:存储区间是连续,空间复杂也很大,时间复杂为O(1)。随机读取效率高,插入和删效率低。
       链表:区间离散,空间复杂度小,时间复杂度O(n)。插入删除效率高。
       哈希表:数组+链表,以实现查询效率高和插入删除效率也高。时间复杂度O(1)~O(n)
       红黑树:时间复杂度O(logn)。

       基本原理:JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

      

       put(k,v)实现原理

       1.如果table为空或者长度为0,那么使用resize()方法扩容
       2.计算插入存储的数组下标i
       3.如果数组为空,即不存在Hash冲突,则直接插入数组;
          如果数组不为空,即发生Hash冲突:
              a.检查数组的第一个Node,利用equals比较key,相等直接覆盖;否则转b
              b.继续判断,需要插入的数据结构是红黑树还是链表,如果是红黑树,则直接插入;否则转c
              c.需要插入的数据结构是链表,则利用equals比较key,若存在则覆盖;否则插在链表尾部。插入完成后判断如果链表长度达到8,则存储结构改为红黑树(treeifyBin方法改红黑树前会先判断hashMap的长度,如小于64只进行resize,大于64才改红黑树)
       4.插入成功后,判断下是否需要resize。

       get(k)实现原理

       1.计算插入存储的数组下标i
       2.检查数组的第一个Node,利用equals比较key,是要找的就直接返回;
       3.如果不是则判定下该table[i]是红黑树还是链表,然后到对应的数据结构下利用equals比较key去查找。

       数组下标计算

       i = (n-1) & hash,hash的计算是return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16;
       为什么计算hash要右移16位:如果直接使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,与自己右移16位进行异或(当数组长度为2的幂次方时,h&(length-1)等价于h%length),进一步降低hash碰撞的概率。

       总结一下HashMap是使用了哪些方法来有效解决哈希冲突的

       1. 使用链表;
       2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
       3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快。

       扩容机制resize()

       创建HashMap时,如果不指明初始大小,默认大小为16(即数组大小16),如果HashMap中的元素个数达到(负载因子 x 容量),则重新调整HashMap大小变为原来2倍,扩容很耗时。
       大致步骤:
       1.先确定是哪种情况扩容:a.初始化哈希表;b.当前数组容量过小,需要扩容
       2.把每一个bucket都移动到新的bucket中去,这是扩容的一个主要开销来源。

       如果key是对象类型,就必须重写hashcode()和equals()的原因

       1.重写equals()因为比较元素时要用;
       2.如果你重写了equals(),而保留hashCode()的实现不变,那么很可能某两个对象明明是“相等”,而hashCode()却不一样。所以Java规定重写equals(),必须重写hashcode()。前者相等后者必须相等。

       HashMap线程安全问题

       JDK1.7的扩容时循环链问题:并发环境下,需要扩容时候出现循环链表,通过get获取值造成死循环,CPU飙升。(扩容时数据会发生数据迁移transfer(),迁移的过程就是一个rehash()的过程,多个线程同时操作导致Entry的next被并发修改,就有可能会形成循环链表)。JDK1.8引入了红黑树优化数组链表,同时改成了尾插(新增元素在链表尾部,JDK1.7是头插),理论上是不会有环了。另说明,JDK1.8中没有transfer()方法,而是在resize()中完成了数据迁移。
       JDK1.7的扩容时数据丢失:也是因为transfer(),并发时next被提前置为null等原因。
       JDK1.8在并发执行put操作时会发生数据覆盖:因为并发赋值时数据会被覆盖。

ConcurrentHashMap

       整体结构

       JDK1.7:ReentrantLock+Segment+HashEntry
       JDK1.8:Node数组+链表+红黑树的数据结构来实现,与HashMap结构相似,采用synchronized+CAS保证线程安全
       JDK1.8总体优化:
       1.取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
       2.JDK1.7采用Segment的分段锁机制实现线程安全,其中Segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。直接锁bucket,降低锁的粒度。
       3.引入了红黑树结构,从原来的遍历链表O(n),变成遍历红黑树O(logn)。总体复杂度O(1)~O(logn)。

       put()

       JDK1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。
       JDK1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似JDK1.7)

       get()

       两者基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁。

       resize()

       JDK1.7:跟HashMap步骤一样,只不过是搬到单线程中执行,避免了HashMap在1.7中扩容时死循环的问题,保证线程安全。
       JDK1.8:支持并发扩容,与JDK1.8的HashMap一样扩容时由头插改为尾插(为了避免循环链问题),迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。

       size()

       JDK1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的Segment求和。
       JDK1.8:CAS修改baseCount值,失败的线程会继续执行方法体中的逻辑,使用CounterCell记录元素个数的变化。累加baseCount和CounterCell数组中的数量,即可得到元素的总个数。

TreeMap

       红黑树

       红黑树是一种特定类型的二叉树,它维持大致上的平衡,适合频繁的插入和删除,可以达到O(logn)的时间复杂度。

       put()操作

       根据二叉查找树的特性,遍历找到新节点合适的插入位置,最后通过fixAfterInsertion(e)方法来进行自平衡处理(进行重新着色和左旋右旋操作,保证红黑树在进行插入节点之后,仍然是一颗红黑树)。(左旋:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点)

       get()操作

       get方法是通过二分查找的思想。

       remove()操作

       先是找到这个节点,直接调用了getEntry(Object key),通过deleteEntry(p)进行删除操作,最后通过fixAfterDeletion进行自平衡操作。

3.反射机制

反射是什么

       JAVA反射机制是在运行状态中:
       对于任意一个类,都能够知道这个类的所有属性和方法;
       对于任意一个对象,都能够调用它的任意一个方法和属性;
       这种动态获取的信息以及动态调用对象的方法的功能称为java的反射机制
       程序中对象的类型一般都是在编译期就确定下来的,而当我们的程序在运行时,可能需要动态的加载一些类,这些类因为之前用不到,所以没有加载到jvm,这时,使用Java反射机制可以在运行期动态的创建对象并调用其属性,它是在运行时根据需要才加载。

反射的原理

       下图是类的正常加载过程,反射原理与class对象:
       Class对象的由来是将.class文件读入内存,并为之创建一个Class对象。

      

获取反射入口(class对象)的三种方法

       1.Class.forName("全类名")
       2.类名.class
       3.对象.getClass()

通过反射来生成对象的两种方法

       1.Class对象的newInstance()方法

Class<?> c = String.class;
Object str = c.newInstance();

       2.先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法

//获取String的Class对象
Class<?> str = String.class;
//通过Class对象获取指定的Constructor构造器对象
Constructor constructor=c.getConstructor(String.class);
//根据构造器创建实例:
Object obj = constructor.newInstance(“hello reflection”);

反射的用途或场景

       1.根据反射入口对象(class)获取类的各种信息(所有public方法、构造方法等)
       2.通过反射获取对象的实例,并操作对象(调用对象的方法)
       3.当我们在使用IDE,比如Ecplise时,当我们输入一个对象或者类,并想调用他的属性和方法是,一按点号,编译器就会自动列出他的属性或者方法,这里就是用到反射。
       4.反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean、Spring IOC),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。

反射的缺点

       1.由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。
       2.另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

4.动态代理

代理模式

       代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托了(真实对象)预处理消息、过滤消息、传递消息给委托类,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理。
       通过代理静默地解决一些业务无关的问题,比如远程、安全、事务、日志、资源关闭。

       静态代理:事先写好代理类,可以手工编写,也可以用工具生成。在程序运行之前,代理类的.class文件就已经生成。缺点是每个业务类都要对应一个代理类,非常不灵活。
       动态代理:代码运行期间加载被代理的类这就是动态代理。缺点是生成代理对象和调用代理方法都要额外花费时间。
      
       Java 反射机制的常见应用:动态代理(AOP、RPC)、提供第三方开发者扩展能力(Servlet容器,JDBC连接)、第三方组件创建对象(DI)。

JDK动态代理

       JDK动态代理:基于Java反射机制实现,必须要是接口的实现类才能用这种办法生成代理对象。新版本也开始结合ASM机制。

       使用方法

       1.创建一个接口

public interface Subject {
    void hello(String param);
}

       2.实现接口

public class SubjectImpl implements Subject {
    @Override
    public void hello(String param) {
        System.out.println("hello  " + param);
    }
}

       3.创建SubjectImpl的代理类

public class SubjectProxy implements InvocationHandler {
    private Object target;

    public SubjectProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("--------------begin-------------");
        Object invoke = method.invoke(target, args);
        System.out.println("--------------end-------------");
        return invoke;
    }
}

       4.编写代理类实际的调用,利用Proxy类创建代理之后的Subject类。

public class Main {

    public static void main(String[] args) {
        Subject subject = new SubjectImpl();
        InvocationHandler subjectProxy = new SubjectProxy(subject);
        Subject proxyInstance = (Subject) Proxy.newProxyInstance(subjectProxy.getClass().getClassLoader(), subject.getClass().getInterfaces(), subjectProxy);
        proxyInstance.hello("world");
    }
}

       原理解析

       调用Proxy.newProxyInstance生成代理类的实现类:
       1.调用getProxyClass0寻找或生成指定代理类
       2.缓存调用ProxyClassFactory生成代理类,proxy class的生成最终调用ProxyClassFactory的apply方法
       3.ProxyGenerator.generateProxyClass使用生成的代理类的名称、接口、访问标志,生成proxyClassFile字节码
       4.生成字节码之后利用反射生成实例

CGLIB动态代理

       CGLIB动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。(ASM 是一个 Java 字节码操控框架。)

       使用方法

       1.引入CGLIB的jar包
       2.创建代理类

public class CGsubject {
    public void sayHello(){
        System.out.println("hello world");
    }
}

       3.实现MethodInterceptor接口,对方法进行拦截处理。

public class HelloInterceptor implements MethodInterceptor{
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("begin time -----> "+ System.currentTimeMillis());
        Object o1 = methodProxy.invokeSuper(o, objects);
        System.out.println("end time -----> "+ System.currentTimeMillis());
        return o1;
    }
}

       4.创建被代理类,利用Enhancer来生产被代理类,这样可以拦截方法

public class Main {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(CGsubject.class);
        enhancer.setCallback(new HelloInterceptor());
        CGsubject cGsubject = (CGsubject) enhancer.create();
        cGsubject.sayHello();
    }

}

       原理解析

       Cglib是一个优秀的动态代理框架,它的底层使用ASM在内存中动态的生成被代理类的子类,使用CGLIB即使代理类没有实现任何接口也可以实现动态代理功能。CGLIB具有简单易用,它的运行速度要远远快于JDK的Proxy动态代理。
       CGLIB的核心类:
       net.sf.cglib.proxy.Enhancer – 主要的增强类
       net.sf.cglib.proxy.MethodInterceptor – 主要的方法拦截类,它是Callback接口的子接口,需要用户实现
       net.sf.cglib.proxy.MethodProxy – JDK的java.lang.reflect.Method类的代理类,可以方便的实现对源对象方法的调用

5.序列化

什么是序列化与反序列化

       Java 序列化是指把 Java 对象转换为字节序列的过程;
       Java 反序列化是指把字节序列恢复为 Java 对象的过程。

为什么要序列化与反序列化

       1.把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
       2.在网络上传送对象的字节序列。
       场景:
       1.Web 容器就会把一些 Session 或者图片视频等先序列化,让他们离开内存空间,序列化到硬盘中,当需要调用时(用户再次访问),再把保存在硬盘中的对象还原到内存中。
       2.两个进程进行远程通信时,彼此可以发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。

序列化方法

       Java原生序列化

       实现Serializable接口(隐式序列化):使用ObjectInputStream的writeObject()和ObjectOutputStream的readObject()就能实现反序列化和序列化。
       缺点:三种序列化方法中最低效的一种。

       Json序列化

       使用Alibaba的FastJSON。
       优点:有极快的性能,具备可读性 ,不需要对要序列化的类做特殊处理。

JSON.toJSONString(user);     
JSON.parseObject(text, User.class);

       Google 的protobuf

       原始的ProtoBuff需要自己写.proto文件,通过编译器将其转换为java文件,显得比较繁琐。百度研发的jprotobuf框架将Google原始的protobuf进行了封装,对其进行简化,仅提供序列化和反序列化方法。
       优点:跨语言;序列化后数据占用空间比JSON小。三种方法中效率最高的一种。
       缺点:需要配置或者标注。但jprotobuf框架已解决。

@Protobuf(fieldType = FieldType.INT32, required = false, order = 1)
private Integer userId;

Codec<User> studentClassCodec = ProtobufProxy.create(User.class, false);
studentClassCodec.encode(u2);
studentClassCodec.decode(bytes);

 

三、其他Java生态相关

1.maven相关

maven依赖原则

maven依赖原则总结起来就两条:路径最短,声明顺序其次。

路径最短优先

如 E->F->D2 比 A->B->C->D1 路径短。Maven 面对 D1 和 D2 时,会默认选择最短路径的那个 jar 包,即 D2。

最先声明优先

如果路径一样的话,如: A->B->C1, E->F->C2 ,两个依赖路径长度都是 2,那么就选择最先声明。

 

scope的使用

compile

默认的scope,表示 dependency 都可以在生命周期中使用。而且,这些dependencies 会传递到依赖的项目中。适用于所有阶段,会随着项目一起发布。

provided

跟compile相似,但是表明了dependency 由JDK或者容器提供,例如Servlet AP和一些Java EE APIs。这个scope 只能作用在编译和测试时,同时没有传递性。
一般通过添加<scope>provided</scope>,表明该包只在编译和测试的时候用,减少运行时冲突的可能。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值