秦怀杂货店

秦怀杂货店 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 aphysia.cn 编辑
编辑

山高水长,纵使缓慢,驰而不息。
公众号:秦怀杂货店

个人动态

秦怀杂货店 发布了文章 · 2020-12-26

JVM笔记【1】-- 运行时数据区

[TOC]

(一)java内存区域管理

C/C++每一个new操作都需要自己去delete/free,而java里面有虚拟机自动管理内存,不容易出现内存泄漏或者溢出的问题,但是不容易出现不代表不出现,了解虚拟机怎么使用和管理内存是十分重要的是,对程序优化或者问题排查有帮助。

运行时区域主要分为:

  • 线程私有:

    • 程序计数器:Program Count Register,线程私有,没有垃圾回收
    • 虚拟机栈:VM Stack,线程私有,没有垃圾回收
    • 本地方法栈:Native Method Stack,线程私有,没有垃圾回收
  • 线程共享:

    • 方法区:Method Area,以HotSpot为例,JDK1.8后元空间取代方法区,有垃圾回收。
    • 堆:Heap,垃圾回收最重要的地方。

image-20201222221827719

1.1 程序计数器

空间很小,当前线程执行的字节码的行号指示器(线程独有,指示当前执行到哪,下一步需要执行哪一个字节码),分支,循环,跳转,异常处理,线程恢复都需要依赖它。
线程私有:java多线程其实是线程轮流切换并分配处理器执行时间的方式实现,一个核一个具体的时间点,只会执行一个线程的指令。线程切换需要保存和恢复正确的执行位置(保护和恢复现场),所以不同的线程需要不同的程序计数器。

  • 执行java方法时,程序计数器记录的是正在执行的字节码指令地址
  • 执行Native方法,程序计数器为空

唯一一个没有规定任何OutOfMemory的区域,也没有GC(垃圾回收)。

1.2 虚拟机栈

线程私有,生命周期和线程一样,主要是记录该线程Java方法执行的内存模型。虚拟机栈里面放着好多栈帧。注意虚拟机栈,对应是Java方法,不包括本地方法。
一个Java方法执行会创建一个栈帧,一个栈帧主要存储:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法出口

每一个方法调用的时候,就相当于将一个栈帧放到虚拟机栈中(入栈),方法执行完成的时候,就是对应着将该栈帧从虚拟机栈中弹出(出栈)。

每一个线程有一个自己的虚拟机栈,这样就不会混起来,如果不是线程独立的话,会造成调用混乱。

大家平时说的java内存分为堆和栈,其实就是为了简便的不太严谨的说法,他们说的栈一般是指虚拟机栈,或者虚拟机栈里面的局部变量表。

局部变量表一般存放着以下数据:

  • 基本数据类型(boolean,byte,char,short,int,float,long,double
  • 对象引用(reference类型,不一定是对象本身,可能是一个对象起始地址的引用指针,或者一个代表对象的句柄,或者与对象相关的位置)
  • returAddress(指向了一条字节码指令的地址)

局部变量表内存大小编译期间确定,运行期间不会变化。空间衡量我们叫Slot(局部变量空间)。64位的long和double会占用2个Slot,其他的数据类型占用1个Slot。

异常:

  • StackOverflowError:线程请求的栈深度大于虚拟机允许的深度
  • OutOfMemoryError:内存不足

1.3 本地方法栈

和虚拟机栈类似,对应本地方法,Native,虚拟机规范允许语言,使用方式和数据结构不同,有些可能将虚拟机栈和本地方法栈合并。
异常与虚拟机栈一致:

  • StackOverflowError:线程请求的栈深度大于虚拟机允许的深度
  • OutOfMemoryError:内存不足

1.4 java堆

堆是内存管理最大的一块,线程共享。

虚拟机规范中说,所有的对象实例和数组都要在堆上分配。但是实际上不是所有的对象都在堆上分配,这个和JIT编译器的发展和逃逸分析技术相关。Why?
// TODO
堆的细分:新生代,老年代,再细分有Eden,From survivor,To survivor等。

堆中也有可能有线程私有的区域,分配缓冲区。

物理上可以不连续,但是逻辑上是连续的。

异常:

  • OutOfMemoryError:内存不足

1.5 方法区

名为非堆,但是实际和堆一样,是线程共享的区域,主要存贮以下信息:

  • 已被虚拟机加载的类信息
  • 常量
  • 静态变量
  • 即时编译器编译后的代码

方法区不等于永久代,指示Hotspot虚拟机将GC分代收集拓展到方法区,也就是用永久代实现了方法区,而其他的虚拟机不一定,不是固定的。JDK1.7将永久代的字符串常量移出了。

方法区回收垃圾的效果不是很好,可以选择不回收,虚拟机可以决定,当然也可能发生内存泄漏。
异常:

  • OutOfMemoryError:内存分配异常

1.5.1 运行时常量池

运行时常量池时方法区的一部分,但是不是全部,Class文件主要包括:

  • 类的版本
  • 字段
  • 方法
  • 接口
  • 常量池,存放编译产生的字面量和符号引用,一般除了描述Class文件的符号引用,还有直接引用也在里面。是动态的,运行时可以产生,比如String.intern()方法。

异常:

  • OutOfMemoryError:内存分配异常

(二)直接内存

不是虚拟机运行时数据区,也不是规范规定的区域,但是使用频繁且可能会有OutOfMemoryError:内存分配异常出现。
比如,NIO(1.4)基于Channel与Buffer的I/O,可以用Native函数直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为引用来操作,提高性能,不需要Java堆和Native堆都来回复制数据。

直接内存受物理的内存,或者处理器寻址空间之类的限制。

本文系JVM学习相关笔记,整理来自周志明老师的《深入理解Java虚拟机》,无比钦佩,强烈推荐!

【作者简介】
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

查看原文

赞 0 收藏 0 评论 0

秦怀杂货店 发布了文章 · 2020-12-26

Mybatis【11】-- Mybatis Mapper动态代理怎么写?

[TOC]

1.回顾Mybatis执行sql的流程

在之前的代码中我们的运行过程再梳理一下,首先我们执行Test,调用dao接口方法

接口的定义:

调用接口的实现类方法:

最后才是调用真正的sql:

上面的代码是在接口实现类里面自己去执行id,查找并执行mapper文件里面的sql,那么我们想是不是可以减少一步呢?

如果我们不用自己实现接口,只需要将接口的名字和mapper文件的namespace对应起来,将接口里面的方法名与sql语句标签的id对应起来是不是就可以了呢?

事实上,mybatis提供了这样的做法,这就是mapper动态代理。

2.mapper动态代理怎么写?

首先主配置文件(Mybatis.xml),在里面配置数据库连接信息,注册需要扫描的mapper文件:

定义数据库查询的接口,里面每一个接口的名字很重要,需要和mapper里面每一条sql对应起来:

定义mapper文件(namespace是接口的全限定类名):

那我们在使用的时候,需要使用sqlSession.getMapper()方法,里面传入的是接口,意思是通过接口的全限定名,也就是前面在mapper.xml文件里面配置的命名空间nameSpace,这样一来,就是获取到了代理类,将daomapper.xml文件关联起来了,而每条sqlid与我们的接口方法名字对应起来)

我们在前面还写到过一个selectStudentMap()方法,但是里面调用的是和SelectList()一样的sql,在接口的实现类里面我们自己处理了一下,但是现在使用自动实现的话,底层只会调用SelectOne()或者SelectList()方法,所以这个方法会报错,如果接受类型是list,那么框架会自动使用selectList()方法,否则就会选择selectOne()这个方法。

在这里我们使用的是返回的是map,所以自动选择返回selectOne()方法,那么就会报错。如果我们需要使用自动返回map的话,可以自己定一个map,或者返回list之后再处理,这个知识点后面再介绍,有兴趣可以访问:mybatis的mapper返回map结果集

3.mapper动态代理怎么做的?

打一个断点在sqlSession.getMapper()方法上:

我们可以看到执行下面的接口方法(接口SqlSession的方法)

<T> T getMapper(Class<T> var1);

这是一个接口,我们可以看到实现接口的有两个类,一个是DefaultSqlSession,一个是SqlSessionManager,我们需要看的是DefaultSqlSession下面的接口:

 public <T> T getMapper(Class<T> type) {
    return this.configuration.getMapper(type, this);
  }

我们知道,在创建sqlsession的时候,confiiguration这个配置对象已经创建完成。跟进去,这是使用mapper注册器对象的getMapper()方法,将当前的sqlSession对象传递进去:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

我们跟进去源码,可以发现里面使用knownMappers.get(type)来获取mapper代理工厂,这个konwnMappers是一个hashMap,这个hashMap里面已经初始化了mapperProxyFactory对象了,获取到工厂对象之后,再去使用sqlSession实例化:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

实例化的时候,使用了mapper动态代理:

public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

从下面的debug结果中我们可以看到,这是动态代理的结果,我们看到的是dao,但是动态代理对这个dao做了增强,实则是一个mapperProxy

【作者简介】
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

查看原文

赞 0 收藏 0 评论 0

秦怀杂货店 发布了文章 · 2020-12-26

设计模式【2】-- 简单工厂模式了解一下?

[TOC]

1.简单工厂模式介绍

工厂模式,比较常用,属于创建型模式,也就是主要是用来创建对象的。工厂模式,有三种,主要分为:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

其中,本文要讲的就是,简单工厂模式,但是简单工厂模式,并不是属于GoF讲的23种设计模式中。简单工厂模式,也叫静态工厂方法模式。简单而言,就是有一个具体的工厂类,用来生产不同类型的对象,而这些对象,都有相似的特点,它们都实现同一个接口。

什么时候应该使用工厂模式?为什么需要工厂模式呢?

工厂模式主要是用来生成不同的对象,也就是屏蔽了对象生成的时候的复杂性,使用的时候不需要知道对象是怎么生成的,而只需要关注要生成什么对象。如果构造一个对象特别的费劲,而我们又经常需要构造生成这个对象,那么使用工厂模式是比较有利的。我们都知道,设计模式主要就是为了设计出更加简洁,易懂,方便维护,方便拓展的代码。

如果一个很复杂的对象,要在多个地方构建,那么要是改动一次,我们就需要找出所有引用的地方,逐一修改,那会很麻烦。

简单工厂模式主要有三种角色:

  • 简单工厂:负责创建所有的实例,依照不同的类型创建不同的对象,也就是产品。
  • 抽象产品接口:也就是所有产品的一个抽象,一般是所有产品都需要实现的接口。
  • 具体产品:实现抽象产品接口,不同的产品做不一样的实现。

2.简单工厂模式举例

假设现在有一个果园,用来种植各种水果,但是每一种水果种植的方式又不一样。首先,先定义一个接口Fruit:

public interface Fruit {
    public void process();
}

定义三种水果ApplePearOrange:

public class Apple implements Fruit{
    public void process() {
        System.out.println("I am an Apple");
    }
}
public class Pear implements Fruit{
    public void process() {
        System.out.println("I am a Pear");
    }
}
public class Orange implements Fruit{
    public void process() {
        System.out.println("I am an Orange");
    }
}

创建一个工厂类:

public class FruitFactory {
    public static Fruit getFruit(String name) {
        if ("Apple".equalsIgnoreCase(name)) {
            return new Apple();
        } else if ("Pear".equalsIgnoreCase(name)) {
            return new Pear();
        } else if ("Orange".equalsIgnoreCase(name)) {
            return new Orange();
        }
        return null;
    }
}

测试代码如下:

public class FruitTest {
    public static void main(String[] args) {
        Fruit apple = FruitFactory.getFruit("Apple");
        apple.process();
        Fruit pear = FruitFactory.getFruit("Pear");
        pear.process();
        Fruit orange = FruitFactory.getFruit("Orange");
        orange.process();
    }
}

测试结果如下:

<img data-original="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20201218231609645.png" alt="image-20201218231609645" style="zoom:50%;" />

这样的写法,如果后续再来了一种水果,那么只需要实现接口,同时在工厂中增加一个case即可。

3.简单工厂模式的优劣

优点:

  • 产品和工厂的职责比较分明,工厂负责创建,产品负责自己的实现
  • 产生/构建产品比较简单,不需要关注内部细节,只需要知道自己想要哪一种。
  • 增加或者修改产品比较简单,解耦合。

凡事都有优劣,简单工厂方法的缺点在于:

  • 工厂类的重要性很高,一旦出现问题,影响所有的产品。
  • 产品数量一旦特别多的时候,工厂内部逻辑会比较复杂,不利于理解和维护。
  • 静态方法不利于继承和实现。

从以上的优劣,我们可以知道,其实如果产品创建过程比较复杂,而且个数不多,都是依靠某些参数来创建的话,抽象出简单工厂模式,其实是比较有利的一种做法。

查看原文

赞 0 收藏 0 评论 0

秦怀杂货店 发布了文章 · 2020-12-26

设计模式【1.3】-- 为什么饿汉式单例是线程安全的?

我们都知道,饿汉式单例是线程安全的,也就是不会初始化的时候创建出两个对象来,但是为什么呢?

首先定义一个饿汉式单例如下:

 public class Singleton {
    // 私有化构造方法,以防止外界使用该构造方法创建新的实例
    private Singleton(){
    }
    // 默认是public,访问可以直接通过Singleton.instance来访问
    static Singleton instance = new Singleton();
}

之所以是线程安全的,是因为JVM在类加载的过程,保证了不会初始化多个static对象。类的生命周期主要是:

加载-->验证-->准备-->解析-->初始化-->使用-->卸载

上面的代码,实际上类成员变量instance是在初始化阶段的时候完成初始化,所有的类变量以及static静态代码块,都是在一个叫clinit()的方法里面完成初始化。这一点,使用jclasslib可以看出来:

clinit()方法是由虚拟机收集的,包含了static变量的赋值操作以及static代码块,所以我们代码中的static Singleton instance = new Singleton();就是在其中。虚拟机本身会保证clinit()代码在多线程并发的时候,只会有一个线程可以访问到,其他的线程都需要等待,并且等到执行的线程结束后才可以接着执行,但是它们不会再进入clinit()方法,所以是线程安全的。我们可以验证一下:

首先改造一下单例:

public class Singleton {
    // 私有化构造方法,以防止外界使用该构造方法创建新的实例
    private Singleton() {
    }

    // 默认是public,访问可以直接通过Singleton.instance来访问
    static Singleton instance = null;

    static {
        System.out.println("初始化static模块---开始");
        instance = new Singleton();
        try {
            System.out.println("初始化中...");
            Thread.sleep(20 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("初始化static模块----结束");
    }
}

测试代码:

import java.io.*;
import java.lang.reflect.InvocationTargetException;

public class SingletonTests {
    public static void main(String[] args) throws Exception, InvocationTargetException, InstantiationException, NoSuchMethodException {
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                System.out.println("线程1开始尝试初始化单例");
                Singleton singleton = Singleton.instance;
                System.out.println("线程1获取到的单例:" + singleton);
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                System.out.println("线程2开始尝试初始化单例");
                Singleton singleton = Singleton.instance;
                System.out.println("线程2获取到的单例:" + singleton);
            }
        });
        thread1.start();
        thread2.start();
    }
}

运行结果,一开始运行的时候,我们可以看到线程1进去了static代码块,它在初始化,线程2则在等待。

image-20201217141915904

待到线程1初始化完成的时候,线程2也不会再进入static代码块,而是和线程1取得同一个对象,由此可见,static代码块实际上就是线程安全的。

image-20201217143603156

查看原文

赞 0 收藏 0 评论 0

秦怀杂货店 发布了文章 · 2020-12-26

设计模式【1.2】-- 枚举式单例有那么好用么?

[TOC]

1. 单例是什么?

单例模式:是一种创建型设计模式,目的是保证全局一个类只有一个实例对象,分为懒汉式和饿汉式。所谓懒汉式,类似于懒加载,需要的时候才会触发初始化实例对象。而饿汉式正好相反,项目启动,类加载的时候,就会创建初始化单例对象。

前面说过单例模式以及如何破坏单例模式,我们一般情况尽可能阻止单例模式被破坏,于是各种序列化,反射,以及克隆的手段,我们都需要考虑进来,最终的代码如下:


import java.io.Serializable;

public class Singleton implements Serializable {
    private static int num = 0;

      // valitile禁止指令重排
    private volatile static Singleton singleton;

      // 禁止多次反射调用构造器
    private Singleton() {
        synchronized (Singleton.class) {
            if (num == 0) {
                num++;
            } else {
                throw new RuntimeException("Don't use this method");
            }
        }
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
      // 禁止序列化的时候,重新生成对象
    private Object readResolve() {
        return singleton;
    }
}

前面提过破坏序列化的四种方式:

  • 没有将构造器私有化,可以直接调用。
  • 反射调用构造器
  • 实现了cloneable接口
  • 序列化与反序列化

2. 枚举的单例可以被破坏么?

但是突然想到一个问题,一般都说枚举的方式实现单例比较好,较为推荐。真的是这样么?这样真的是安全的么?

那我们就试试,看看各种手段,能不能破坏它的单例。首先我们来写一个单例枚举类:

public enum SingletonEnum {
    INSTANCE;
    public SingletonEnum getInstance(){
        return INSTANCE;
    }
}

在命令行执行以下的命令看上面的枚举类编译之后到底是什么东西?

javac SingletonEnum.java
javap SingletonEnum

public final class singleton.SingletonEnum extends java.lang.Enum<singleton.SingletonEnum> {
  public static final singleton.SingletonEnum INSTANCE;
  public static singleton.SingletonEnum[] values();
  public static singleton.SingletonEnum valueOf(java.lang.String);
  public singleton.SingletonEnum getInstance();
  static {};
}

可以看出,实际上,编译后的代码是继承于Enum类的,并且是泛型。用final修饰,其实也是类,那就是不可以被继承原因。而且INSTANCE也是final修饰的,也是不可变的。但是这样看,上面的都是public方法。那构造方法呢?没有被重写成为private么?

要是没有重写的话,那就很容易破坏单例啊!我们使用javap -p SingletonEnum看看结果:

<img data-original="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201213225659.png" style="zoom:50%;" />

可以看出确实构造函数已经被私有化,那么外部就不能直接调用到构造方法了。那其他方法呢?我们试试放射调用构造器:

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class SingletonTests {
    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        SingletonEnum singleton1 = SingletonEnum.INSTANCE;
        SingletonEnum singleton2 = SingletonEnum.INSTANCE;
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());

        Constructor<SingletonEnum> constructor = null;
        constructor = SingletonEnum.class.getDeclaredConstructor();
        constructor.setAccessible(true);

        SingletonEnum singleton3 = constructor.newInstance();
        System.out.println(singleton1 == singleton3);
    }
}

执行结果如下:

692404036
692404036
Exception in thread "main" java.lang.NoSuchMethodException: singleton.SingletonEnum.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at singleton.SingletonTests.main(SingletonTests.java:15)

咦,怎么回事?反射失败了???

<img data-original="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/20201213231748.png" style="zoom:50%;" />

看起来报错是getDeclaredConstructor()失败了,那我们看看到底有哪些构造器:

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        Constructor<SingletonEnum>[] constructor = null;
        constructor = (Constructor<SingletonEnum>[]) SingletonEnum.class.getDeclaredConstructors();
        for(Constructor<SingletonEnum> singletonEnumConstructor:constructor){
            System.out.println(singletonEnumConstructor);
        }
    }

执行结果如下,发现只有一个构造器,里面参数是Stringint,所以啊,反射调用无参数构造器肯定也是如此。

private singleton.SingletonEnum(java.lang.String,int)

毕竟它是继承于Enum的,那我猜想它大概也只有这个方法,验证以下,打开源码:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
  
    private final String name;

    public final String name() {
        return name;
    }
    private final int ordinal;

    public final int ordinal() {
        return ordinal;
    }

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

可以看出,这里面只有两个属性:nameordinal,构造器被重写了,正是Stringint,验证了我们的猜想,也就是没有办法使用无参数构造器来构造出破坏单例的对象。那要是我们使用有参数构造呢?试试!!!

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class SingletonTests {
    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        SingletonEnum singleton1 = SingletonEnum.INSTANCE;
        SingletonEnum singleton2 = SingletonEnum.INSTANCE;
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());

        Constructor<SingletonEnum> constructor = null;
        constructor = SingletonEnum.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器
        constructor.setAccessible(true);

        SingletonEnum singleton3 = constructor.newInstance("INSTANCE",0);
        System.out.println(singleton1 == singleton3);
    }
}

结果呢?还是一样的报错,这是什么东东?

692404036
692404036
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at singleton.SingletonTests.main(SingletonTests.java:18)

看起来意思是不能反射创建enum对象,啥?这错误一看,就是Constructor.newInstance()417行抛出来的,我们看看:

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        // 限制枚举类型
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

原来反射的源代码中,枚举类型的已经被限制了,一旦调用就会抛出异常,那这条路走不通了,也就证明了反射无法破坏枚举的单例。new对象更是行不通了。

clone呢?打开Enum的源码我们里面就断了这个念头,这里面的clone()方法,已经被final修饰了,不能被子类重写,一调用就抛出异常。所以clone这条路也不可能破坏枚举的单例模式。

    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

那序列化呢?如果我们序列化之后,再反序列化,会出现什么情况?

import java.io.*;
import java.lang.reflect.InvocationTargetException;

public class SingletonTests {
    public static void main(String[] args) throws Exception, InvocationTargetException, InstantiationException, NoSuchMethodException {
        SingletonEnum singleton1 = SingletonEnum.getInstance();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("file"));
        objectOutputStream.writeObject(singleton1);
        File file = new File("file");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        SingletonEnum singleton2 = (SingletonEnum) objectInputStream.readObject();
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

上面的代码执行之后,结果如下:

1627674070
1627674070

说明序列化反序列化回来之后,其实是同一个对象!!!所以无法破坏单例模式。为什么呢?我们来分析一下源码!!!

先看看序列化的时候,实际上调用的是ObjectOutputStream.writeObject(Object obj)

image-20201214102820404

writerObject()Object obj方法里面调用了writeObject0(obj,false),writeObject0(obj,false)里面看到枚举类型的序列化写入:

image-20201214104840375

writeEnum(Enum<?>)里面是怎么序列化的呢?

    private void writeEnum(Enum<?> en,
                           ObjectStreamClass desc,
                           boolean unshared)
        throws IOException
    {
          // 标识是枚举类型
        bout.writeByte(TC_ENUM);
        ObjectStreamClass sdesc = desc.getSuperDesc();
          // 类型描述
        writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
        handles.assign(unshared ? null : en);
          // 将名字写入name()
        writeString(en.name(), false);
    }

看起来序列化的时候,是用名字写入序列化流中,那反序列化的时候呢?是怎么操作的呢?

    public final Object readObject()
        throws IOException, ClassNotFoundException {
        return readObject(Object.class);
    }

里面调用的是另外一个readObject()方法,readObject()方法其实是调用了readObject0(type,false)

image-20201214110032825

看到反序列化的时候,枚举类型的时候,是怎么实现的呢?里面有一个readEnum():

image-20201214110222514

我们来看看readEnum(),里面其实里面是先读取了名字name,再通过名字Enum.valueOf()获取枚举。

image-20201214110933076

所以上面没有使用反射,还是获取了之前的对象,综上所述,枚举的序列化和反序列化并不会影响单例模式。

3. 总结一下

经过上面一顿分析,枚举不可以直接调用构造函数,不可以反射破坏单例模式,因为内部实现阻止了,实现clone接口也不可以,这个方法已经设置为final。序列化和反序列化的时候,内部没有使用反射去实现,而是查找之前的对象,直接返回,所以还是同一个对象。

这样一来,怪不得《effective java》里面推荐这个写法,既简洁,还能够防止各种破坏,还有不用的理由么?

【作者简介】
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

查看原文

赞 0 收藏 0 评论 0

秦怀杂货店 发布了文章 · 2020-12-26

Mybatis【10】-- Mybatis属性名和查询字段名不同怎么做?

很多时候我们有这样的需求,数据库的字段名与实体类的属性名不一致,这个时候我们需要怎么做呢?有两种解决方案,第一种:直接在查询的时候使用别名,将别名设置成与实体类的属性名一致。第二种:使用resultType,自己定义映射关系。
整个项目的目录如下:


首先,我们需要搭建数据库mysql环境(test.sql),id我们写成了sid,name我们写成了sname,age我们写成了sage:

#创建数据库
CREATE DATABASE `test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
#创建数据表
CREATE TABLE `student` ( `sid` INT NOT NULL AUTO_INCREMENT , `sname` VARCHAR(20) NOT NULL ,
`sage` INT NOT NULL , `score` DOUBLE NOT NULL , PRIMARY KEY (`sid`)) ENGINE = MyISAM;

Student.class实体类:

public class Student {
    private Integer id;
    private String name;
    private int age;
    private double score;
    public Student(){

    }
    public Student(String name, int age, double score) {
        super();
        this.name = name;
        this.age = age;
        this.score = score;
    }
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public double getScore() {
        return score;
    }
    public void setScore(double score) {
        this.score = score;
    }
    @Override
    public String toString() {
        return "Student [id=" + id + ", name=" + name + ", age=" + age
                + ", score=" + score + "]";
    }
    
}

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.test</groupId>
    <artifactId>test</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <!-- mybatis核心包 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.3.0</version>
        </dependency>
        <!-- mysql驱动包 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>
        <!-- junit测试包 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <!-- 日志文件管理包 -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.12</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.12</version>
        </dependency>
    </dependencies>
</project>

主配置文件mybatis.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 配置数据库文件 -->
    <properties resource="jdbc_mysql.properties">

    </properties>
    <!-- 别名,对数据对象操作全名太长,需要使用别名 -->
    <typeAliases>
        <!--<typeAlias type="bean.Student" alias="Student"/>-->
        <!--直接使用类名即可,对于整个包的路径配置(别名),简单快捷 -->
        <package name="bean"/>
    </typeAliases>
    <!-- 配置运行环境 -->
    <!-- default 表示默认使用哪一个环境,可以配置多个,比如开发时的测试环境,上线后的正式环境等 -->
    <environments default="mysqlEM">
        <environment id="mysqlEM">
            <transactionManager type="JDBC">
            </transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.user}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 注册映射文件 -->
    <mappers>
        <mapper resource="mapper/mapper.xml"/>
    </mappers>
</configuration>

数据库配置文件(jdbc_mysql.properties):

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC
jdbc.user=root
jdbc.password=123456

日志配置文件 log4j.prperties:

log4j.prpp
log4j.rootLogger=DEBUG, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[service] %d - %c -%-4r [%t] %-5p %c %x - %m%n
log4j.logger.java.sql.Statement = debug
log4j.logger.java.sql.PreparedStatement = debug
log4j.logger.java.sql.ResultSet =debug

使用到的工具类(MyBatisUtils.java):

public class MyBatisUtils {
    static private SqlSessionFactory sqlSessionFactory;

    static public SqlSession getSqlSession() {
        InputStream is;
        try {
            is = Resources.getResourceAsStream("mybatis.xml");
            if (sqlSessionFactory == null) {
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
            }
            return sqlSessionFactory.openSession();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
}

接口定义(IStudentDao.java):

public interface IStudentDao {
    // 返回所有学生的信息List
    public List<Student> selectAllStudents();
    // 根据id查找学生
    public Student selectStudentById(int id);
}

接口实现类(StudentDaoImpl.class):

public class StudentDaoImpl implements IStudentDao {
    private SqlSession sqlSession;
    public List<Student> selectAllStudents() {
        List<Student> students ;
        try {
            sqlSession = MyBatisUtils.getSqlSession();
            students = sqlSession.selectList("selectAllStudents");
            //查询不用修改,所以不用提交事务
        } finally {
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
        return students;
    }

    public Student selectStudentById(int id) {
        Student student=null;
        try {
            sqlSession=MyBatisUtils.getSqlSession();
            student=sqlSession.selectOne("selectStudentById",id);
            sqlSession.commit();
        } finally{
            if(sqlSession!=null){
                sqlSession.close();
            }
        }
        return student;
    }
}

最主要的mapper文件:

可以直接使用别名:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="abc">
    <select id="selectAllStudents" resultType="Student">
        select sid as id,sname as name,sage as age,score from student
    </select>
    <select id="selectStudentById" resultType="Student">
        select sid as id,sname as name,sage as age,score from student where sid=${value}
    </select>
</mapper>

或者可以自己定义映射:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="abc">
    <resultMap id="StudentMapper" type="Student">
        <id column="sid" property="id"/>
        <result column="sname" property="name"/>
        <result column="sage" property="age"/>
    </resultMap>
    <select id="selectAllStudents" resultMap="StudentMapper">
        select sid as id,sname as name,sage as age,score from student
    </select>
    <select id="selectStudentById" resultMap="StudentMapper">
        select sid as id,sname as name,sage as age,score from student where sid=${value}
    </select>
</mapper>



需要注意的点:

  • <resultMap></resultMap>有一个id属性,这个是在其他地方使用的时候的id
  • Type - 实体类,可以写别名,要不就要写带全路径的类名
  • id - 标签是为了标记出作为 ID 的结果可以帮助提高整体性能
  • result – 注入到字段或 JavaBean 属性的普通结果
  • association – 一个复杂类型的关联;许多结果将包装成这种类型嵌套结果映射 – 关联可以指定为一个 resultMap 元素,或者引用一个
  • collection – 一个复杂类型的集合

嵌套结果映射 – 集合可以指定为一个 resultMap 元素,或者引用一个

  • discriminator – 使用结果值来决定使用哪个 resultMap

case – 基于某些值的结果映射
嵌套结果映射 – 一个 case 也是一个映射它本身的结果,因此可以包含很多相 同的元素,或者它可以参照一个外部的 resultMap。
如果对象名与属性名一致,我们可以不把它写入<resultMap></resultMap>

测试类MyTest.class:

public class MyTest {
    private IStudentDao dao;
    @Before
    public void Before(){
        dao=new StudentDaoImpl();
    }
    /*
     * 查询列表
     *
     */
    @Test
    public void testselectList(){
        List<Student> students=dao.selectAllStudents();
        if(students.size()>0){
            for(Student student:students){
                System.out.println(student);
            }
        }
    }
    /*
     * 通过id来查询student
     *
     */
    @Test
    public void testselectStudentById(){
        Student student=dao.selectStudentById(1);
        System.out.println(student);
    }

}

【作者简介】
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

查看原文

赞 0 收藏 0 评论 0

秦怀杂货店 发布了文章 · 2020-12-26

设计模式【1.1】-- 你想如何破坏单例模式?

[TOC]

1.单例是什么?

单例模式:是一种创建型设计模式,目的是保证全局一个类只有一个实例对象,分为懒汉式和饿汉式。所谓懒汉式,类似于懒加载,需要的时候才会触发初始化实例对象。而饿汉式正好相反,项目启动,类加载的时候,就会创建初始化单例对象。

1.1 优点

如果只有一个实例,那么就可以少占用系统资源,节省内存,访问也会相对较快。比较灵活。

1.2 缺点

不能使用在变化的对象上,特别是不同请求会造成不同属性的对象。由于Spring本身默认实例就是单例的,所以使用的时候需要判断应用场景,要不会造成张冠李戴的现象。而往往操作引用和集合,就更不容易查找到这种诡异的问题。例如:一些配置获取,如果后期使用需要修改其值,要么定义使用单例,后期使用深拷贝,要么不要使用单例。

既然使用单例模式,那么就得想尽一切办法,保证实例是唯一的,这也是单例模式的使命。但是代码是人写的,再完美的人也可能写出不那么完美的代码,再安全的系统,也有可能存在漏洞。既然你想保证单例,那我偏偏找出方法,创建同一个类多个不同的对象呢?这就是对单例模式的破坏,到底有哪些方式可以破坏单例模式呢?主要但是不限于以下几种:

  • 没有将构造器私有化,可以直接调用。
  • 反射调用构造器
  • 实现了cloneable接口
  • 序列化与反序列化

2. 破坏单例的几种方法

2.1 通过构造器创建对象

一般来说,一个稍微 ✔️ 的单例模式,是不可以通过new来创建对象的,这个严格意义上不属于单例模式的破坏。但是人不是完美的,写出的程序也不可能是完美的,总会有时候疏忽了,忘记了将构造器私有化,那么外部就可以直接调用到构造器,自然就可以破坏单例模式,所以这种写法就是不成功的单例模式。

/**
 * 下面是使用双重校验锁方式实现单例
 */
public class Singleton{
    private volatile static Singleton singleton;
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

上面就是使用双重检察锁的方式,实现单例模式,但是忘记了写private的构造器,默认是有一个public的构造器,如果调用会怎么样呢?

    public static void main(String[] args) {
        Singleton singleton = new Singleton();
        Singleton singleton1 = new Singleton();
        System.out.println(singleton.hashCode());
        System.out.println(singleton1.hashCode());
        System.out.println(Singleton.getSingleton().hashCode());
    }

运行的结果如下:

692404036
1554874502
1846274136

三个对象的hashcode都不一样,所以它们不是同一个对象,这样也就证明了,这种单例写法是不成功的。

2.2 反射调用构造器

如果单例类已经将构造方法声明成为private,那么暂时无法显式的调用到构造方法了,但是真的没有其他方法可以破坏单例了么?

答案是有!也就是通过反射调用构造方法,修改权限。

比如一个看似完美的单例模式:

import java.io.Serializable;

public class Singleton{

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

测试代码如下:

import java.lang.reflect.Constructor;

public class SingletonTests {
    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.getSingleton();
        Singleton singleton1=Singleton.getSingleton();
        Constructor constructor=Singleton.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Singleton singleton2 =(Singleton) constructor.newInstance(null);

        System.out.println(singleton.hashCode());
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());

    }
}

运行结果:

692404036
692404036
1554874502

从结果我们可以看出:放射确实可以调用到已经私有化的构造器,并且构造出不同的对象,从而破坏单例模式。

那这种情况有没有什么方法可以防止破坏呢?既然要防止破坏,肯定要防止调用私有构造器,也就是调用一次之后,再调用就报错,抛出异常。我们的单例模式可以写成这样:

import java.io.Serializable;

public class Singleton {
    private static int num = 0;

    private volatile static Singleton singleton;

    private Singleton() {
        synchronized (Singleton.class) {
            if (num == 0) {
                num++;
            } else {
                throw new RuntimeException("Don't use this method");
            }
        }
    }

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

测试调用方法不变,测试结果如下,反射调用的时候抛出异常了,说明能够有效阻止反射调用破坏单例的模式:

Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at singleton.SingletonTests.main(SingletonTests.java:11)
Caused by: java.lang.RuntimeException: Don't use this method
    at singleton.Singleton.<init>(Singleton.java:15)
    ... 5 more

2.3 实现了cloneable接口

如果单例对象已经将构造方法声明成为private,并且重写了构造方法,那么暂时无法调用到构造方法。但是还有一种情况,那就是拷贝,拷贝的时候是不需要经过构造方法的。但是要想拷贝,必须实现Clonable方法,而且需要重写clone方法。

import java.io.Serializable;

public class Singleton implements Cloneable {
    private static int num = 0;

    private volatile static Singleton singleton;

    private Singleton() {
        synchronized (Singleton.class) {
            if (num == 0) {
                num++;
            } else {
                throw new RuntimeException("Don't use this method");
            }
        }
    }

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试代码如下:

public class SingletonTests {
    public static void main(String[] args) throws Exception {
        Singleton singleton1=Singleton.getSingleton();
        System.out.println(singleton1.hashCode());
        Singleton singleton2 = (Singleton) singleton1.clone();
        System.out.println(singleton2.hashCode());
    }
}

运行结果如下,两个对象的hashCode不一致,也就证明了如果继承了Cloneable接口的话,并且重写了clone()方法,则该类的单例就可以被打破,可以创建出不同的对象。但是,这个clone的方式破坏单例,看起来更像是自己主动破坏单例模式,什么意思?

也就是如果很多时候,我们只想要单例,但是有极少的情况,我们想要多个对象,那么我们就可以使用这种方式,更像是给自己留了一个后门,可以认为是一种良性的破坏单例的方式。

2.4 序列化破坏单例

序列化,实际上和clone差不多,但是不一样的地方在于我们很多对象都是必须实现序列化接口的,但是实现了序列化接口之后,对单例的保证有什么风险呢?

风险就是序列化之后,再反序列化回来,对象的内容是一样的,但是对象却不是同一个对象了。不信?那就试试看:

单例定义如下:

import java.io.Serializable;

public class Singleton implements Serializable {
    private static int num = 0;

    private volatile static Singleton singleton;

    private Singleton() {
        synchronized (Singleton.class) {
            if (num == 0) {
                num++;
            } else {
                throw new RuntimeException("Don't use this method");
            }
        }
    }

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

测试代码如下:

import java.io.*;
import java.lang.reflect.Constructor;

public class SingletonTests {
    public static void main(String[] args) throws Exception {

        Singleton singleton1 = Singleton.getSingleton();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("file"));
        objectOutputStream.writeObject(singleton1);
        File file = new File("tempFile");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        Singleton singleton2 = (Singleton) objectInputStream.readObject();
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }
}

上面的代码,先将对象序列化到文件,再从文件反序列化回来,结果如下:

2055281021
1198108795

结果证明:两个对象的hashCode不一样,说明这个类的单例被破坏了。

那么有没有方法在这种情况下,防止单例的破坏呢?答案是:有!!!

既然调用的是objectInputStream.readObject()来反序列化,那么我们看看里面的源码,里面调用了readObject()方法。

    public final Object readObject()
        throws IOException, ClassNotFoundException {
        return readObject(Object.class);
    }

readObject()方法,里面调用了readObject0()方法:

    private final Object readObject(Class<?> type)
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        if (! (type == Object.class || type == String.class))
            throw new AssertionError("internal error");

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
              // 序列化对象
            Object obj = readObject0(type, false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

readObject0()内部如下,其实是针对不同的类型分别处理:

    private Object readObject0(Class<?> type, boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                // null
                case TC_NULL:
                    return readNull();
                // 引用类型
                case TC_REFERENCE:
                    // check the type of the existing object
                    return type.cast(readHandle(unshared));
                // 类
                case TC_CLASS:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast a class to java.lang.String");
                    }
                    return readClass(unshared);

                // 代理
                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast a class to java.lang.String");
                    }
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                // 数组
                case TC_ARRAY:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an array to java.lang.String");
                    }
                    return checkResolve(readArray(unshared));

                // 枚举
                case TC_ENUM:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an enum to java.lang.String");
                    }
                    return checkResolve(readEnum(unshared));

                // 对象
                case TC_OBJECT:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an object to java.lang.String");
                    }
                    return checkResolve(readOrdinaryObject(unshared));

                // 异常
                case TC_EXCEPTION:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an exception to java.lang.String");
                    }
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

可以看到处理对象的时候,调用了readOrdinaryObject()方法,好家伙来了:

    private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
              // 反射
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

          // 如果实现了hasReadResolveMethod()方法
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
              // 执行hasReadResolveMethod()方法
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

从上面的diamante可以看出,底层是通过反射来实现序列化的,那我们如果不希望它进行反射怎么办?然后可以看到反射之后,其实有一个查找readResolveMethod()方法有关,如果有实现readResolveMethod(),那就直接调用该方法返回结果,而不是返回反射调用之后的结果。这样虽然反射了,但是不起作用。

那要是我们重写readResolveMethod()方法,就可以直接返回我们的对象,而不是返回反射之后的对象了。

试试?

我们将单例模式改造成为这样:

import java.io.Serializable;

public class Singleton implements Serializable,Cloneable {
    private static int num = 0;

    private volatile static Singleton singleton;

    private Singleton() {
        synchronized (Singleton.class) {
            if (num == 0) {
                num++;
            } else {
                throw new RuntimeException("Don't use this method");
            }
        }
    }

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

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
      // 阻止反序列反射生成对象
    private Object readResolve() {
        return singleton;
    }
}

测试代码不变,结果如下,事实证明确实是这样,反序列化不会重新反射对象了,一直是同一个对象,问题完美解决了。

2055281021
2055281021

3. 小结

一个稍微完美的单例,是不会让别人调用构造器的,但是private的构造器,并不能完全阻止对单例的破坏,如果使用反射还是可以非法调用到构造器,因为我们需要一个次数,构造器如果调用次数过多,那么就直接报错。

但是有时候我们希望留个小后门,所以我们大部分时候不可以破坏单例模式。通过实现cloneable的方式,重写了clone()方法,就可以做到,生成不同的对象。

序列化和clone(),有点像,都是主动提供破坏的方法,但是很多时候不得已提供序列化接口,却不想被破坏,这个时候可以通过重写readResolve()方法,直接返回对象,不返回反射生成的对象,保护了单例模式不被破坏。

查看原文

赞 0 收藏 0 评论 0

秦怀杂货店 发布了文章 · 2020-12-26

设计模式【1】-- 单例模式到底几种写法?

[TOC]

单例模式,是一种比较简单的设计模式,也是属于创建型模式(提供一种创建对象的模式或者方式)。
要点:

    • 1.涉及一个单一的类,这个类来创建自己的对象(不能在其他地方重写创建方法,初始化类的时候创建或者提供私有的方法进行访问或者创建,必须确保只有单个的对象被创建)。
    • 2.单例模式不一定是线程不安全的。
    • 3.单例模式可以分为两种:懒汉模式(在第一次使用类的时候才创建,可以理解为类加载的时候特别懒,要用的时候才去获取,要是没有就创建,由于是单例,所以只有第一次使用的时候没有,创建后就可以一直用同一个对象),饿汉模式(在类加载的时候就已经创建,可以理解为饿汉已经饿得饥渴难耐,肯定先把资源紧紧拽在自己手中,所以在类加载的时候就会先创建实例)

      关键字:

      • 单例:singleton
      • 实例:instance
      • 同步: synchronized

    饿汉模式

    1.私有属性

    第一种singlepublic,可以直接通过Singleton类名来访问。

     public class Singleton {
        // 私有化构造方法,以防止外界使用该构造方法创建新的实例
        private Singleton(){
        }
        // 默认是public,访问可以直接通过Singleton.instance来访问
        static Singleton instance = new Singleton();
    }

    2.公有属性

    第二种是用private修饰singleton,那么就需要提供static 方法来访问。

    public class Singleton {
        private Singleton(){
        }
        // 使用private修饰,那么就需要提供get方法供外界访问
        private static Singleton instance = new Singleton();
        // static将方法归类所有,直接通过类名来访问
        public static Singleton getInstance(){
            return instance;.
        }
    }

    3. 懒加载

    饿汉模式,这样的写法是没有问题的,不会有线程安全问题(类的static成员创建的时候默认是上锁的,不会同时被多个线程获取到),但是是有缺点的,因为instance的初始化是在类加载的时候就在进行的,所以类加载是由ClassLoader来实现的,那么初始化得比较早好处是后来直接可以用,坏处也就是浪费了资源,要是只是个别类使用这样的方法,依赖的数据量比较少,那么这样的方法也是一种比较好的单例方法。
    在单例模式中一般是调用getInstance()方法来触发类装载,以上的两种饿汉模式显然没有实现lazyload(个人理解是用的时候才触发类加载)
    所以下面有一种饿汉模式的改进版,利用内部类实现懒加载。
    这种方式Singleton类被加载了,但是instance也不一定被初始化,要等到SingletonHolder被主动使用的时候,也就是显式调用getInstance()方法的时候,才会显式的装载SingletonHolder类,从而实例化instance。这种方法使用类装载器保证了只有一个线程能够初始化instance,那么也就保证了单例,并且实现了懒加载。

    值得注意的是:静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。

    public class Singleton {
        private Singleton(){
        }
        //内部类
        private static class SingletonHolder{
            private static final Singleton instance = new Singleton();
        }
        //对外提供的不允许重写的获取方法
        public static final Singleton getInstance(){
            return SingletonHolder.instance;
        }
    }

    懒汉模式

    最基础的代码(线程不安全)

    public class Singleton {
        private static Singleton instance = null;
        private Singleton(){
        }
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }

    这种写法,是在每次获取实例instance的时候进行判断,如果没有那么就会new一个出来,否则就直接返回之前已经存在的instance。但是这样的写法不是线程安全的,当有多个线程都执行getInstance()方法的时候,都判断是否等于null的时候,就会各自创建新的实例,这样就不能保证单例了。所以我们就会想到同步锁,使用synchronized关键字:
    加同步锁的代码(线程安全,效率不高)

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

    这样的话,getInstance()方法就会被锁上,当有两个线程同时访问这个方法的时候,总会有一个线程先获得了同步锁,那么这个线程就可以执行下去,而另一个线程就必须等待,等待第一个线程执行完getInstance()方法之后,才可以执行。这段代码是线程安全的,但是效率不高,因为假如有很多线程,那么就必须让所有的都等待正在访问的线程,这样就会大大降低了效率。那么我们有一种思路就是,将锁出现等待的概率再降低,也就是我们所说的双重校验锁(双检锁)。

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

    1.第一个if判断,是为了降低锁的出现概率,前一段代码,只要执行到同一个方法都会触发锁,而这里只有singleton为空的时候才会触发,第一个进入的线程会创建对象,等其他线程再进入时对象已创建就不会继续创建,如果对整个方法同步,所有获取单例的线程都要排队,效率就会降低。
    2.第二个if判断是和之前的代码起一样的作用。

    上面的代码看起来已经像是没有问题了,事实上,还有有很小的概率出现问题,那么我们先来了解:原子操作指令重排

    1.原子操作

    • 原子操作,可以理解为不可分割的操作,就是它已经小到不可以再切分为多个操作进行,那么在计算机中要么它完全执行了,要么它完全没有执行,它不会存在执行到中间状态,可以理解为没有中间状态。比如:赋值语句就是一个原子操作:
     n = 1; //这是一个原子操作 

    假设n的值以前是0,那么这个操作的背后就是要么执行成功n等于1,要么没有执行成功n等于0,不会存在中间状态,就算是并发的过程中也是一样的。
    下面看一句不是原子操作的代码:

    int n =1;  //不是原子操作

    原因:这个语句中可以拆分为两个操作,1.声明变量n,2.给变量赋值为1,从中我们可以看出有一种状态是n被声明后但是没有来得及赋值的状态,这样的情况,在并发中,如果多个线程同时使用n,那么就会可能导致不稳定的结果。

    2.指令重排

    所谓指令重排,就是计算机会对我们代码进行优化,优化的过程中会在不影响最后结果的前提下,调整原子操作的顺序。比如下面的代码:

    int a ;   // 语句1 
    a = 1 ;   // 语句2
    int b = 2 ;     // 语句3
    int c = a + b ; // 语句4

    正常的情况,执行顺序应该是1234,但是实际有可能是3124,或者1324,这是因为语句3和4都没有原子性问题,那么就有可能被拆分成原子操作,然后重排.
    原子操作以及指令重排的基本了解到这里结束,看回我们的代码:

    主要是instance = new Singleton(),根据我们所说的,这个语句不是原子操作,那么就会被拆分,事实上JVM(java虚拟机)对这个语句做的操作:
    • 1.给instance分配了内存
    • 2.调用Singleton的构造函数初始化了一个成员变量,产生了实例,放在另一处内存空间中
    • 3.将instance对象指向分配的内存空间,执行完这一步才算真的完成了,instance才不是null。

    在一个线程里面是没有问题的,那么在多个线程中,JVM做了指令重排的优化就有可能导致问题,因为第二步和第三步的顺序是不能够保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回instance,然后使用,就会报空指针。
    从更上一层来说,有一个线程是instance已经不为null但是仍没有完成初始化中间状态,这个时候有一个线程刚刚好执行到第一个if(instance==null),这里得到的instance已经不是null,然后他直接拿来用了,就会出现错误。
    对于这个问题,我们使用的方案是加上volatile关键字。

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

    volatile的作用:禁止指令重排,把instance声明为volatile之后,这样,在它的赋值完成之前,就不会调用读操作。也就是在一个线程没有彻底完成instance = new Singleton();之前,其他线程不能够去调用读操作。

    • 上面的方法实现单例都是基于没有复杂序列化和反射的时候,否则还是有可能有问题的,还有最后一种方法是使用枚举来实现单例,这个可以说的比较理想化的单例模式,自动支持序列化机制,绝对防止多次实例化。
    public enum Singleton {
        INSTANCE;
        public void doSomething() {
    
        }
    }

    以上最推荐枚举方式,当然现在计算机的资源还是比较足够的,饿汉方式也是不错的,其中懒汉模式下,如果涉及多线程的问题,也需要注意写法。

    最后提醒一下,volatile关键字,只禁止指令重排序,保证可见性(一个线程修改了变量,对任何其他线程来说都是立即可见的,因为会立即同步到主内存),但是不保证原子性。

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-26

    Mybatis【9】-- Mybatis占位符#{}和拼接符${}有什么区别?

    代码直接放在Github仓库【https://github.com/Damaer/Myb... 】,可直接运行,就不占篇幅了。

    [TOC]

    1.#{}占位符

    1.#{}占位符可以用来设置参数,如果传进来的是基本类型,也就是(string,long,double,int,boolean,float等),那么#{}里面的变量名可以随意写,什么abc,xxx等等,这个名字和传进来的参数名可以不一致。

    2.如果传进来的是pojo类型,那么#{}中的变量名必须是pojo的属性名,可以写成属性名,也可以写属性名.属性名

    参数是int,不需要设置parameterType

    <delete id="deleteStudentById" >
        delete from student where id=#{XXXdoukeyi}
    </delete>

    parameterTypepojo类,如果使用pojo类型作为参数,那么必须提供get方法,也就是框架在运行的时候需要通过反射根据#{}中的名字,拿到这个值放到sql语句中,如果占位符中的名称和属性不一致,那么报ReflectionException

    <insert id="insertStudent" parameterType="Student">
        insert into student(name,age,score) values(#{name},#{age},#{score})
    </insert>

    3.#{}占位符不能解决的三类问题:

    动态表名不可以用#{} :Select * from #{table}
    动态列名不可以用#{} : select #{column} from table
    动态排序列不可以用#{} : select * from table order by #{column}

    注意:不能这样写:

    <insert id="insertStudent" parameterType="Student">
        insert into student(name,age,score) values(#{Student.name},#{Student.age},#{Student.score})
    </insert>

    否则会报一个错误(会将Student当成一个属性),所以我们类名就直接省略不写就可以了:

    ### Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'Student' in 'class bean.Student'

    2.${}拼接符

    1.如果传进来的是基本类型,也就是(string,long,double,int,boolean,float等),那么#{}里面的变量名必须写value

    <delete id="deleteStudentById" >
        delete from student where id=${value}
    </delete>

    2.如果传进来的是pojo类型,那么#{}中的变量名必须是pojo的属性名,可以写成属性名,也可以写属性名.属性名但是由于是拼接的方式,对于字符串我们需要自己加引号。

    <insert id="insertStudent" parameterType="Student">
        insert into student(name,age,score) values('${name}',${age},${score})
    </insert>

    与上面一样,不能将类名写进来:

    <!--这是错误的-->
    <insert id="insertStudent" parameterType="Student">
        insert into student(name,age,score) values('${Student.name}',${Student.age},${Student.score})
    </insert>

    3.${}占位符是字符串连接符,可以用来动态设置表名,列名,排序名

    动态表名 :Select * from ${table}
    动态列名 : select ${column} from table
    动态排序 : select * from table order by ${column}

    4.${}可以作为连接符使用,但是这样的方式是不安全的,很容易发生sql注入问题,sql注入问题可以参考https://blog.csdn.net/aphysia...

    <select id="selectStudentsByName" resultType="Student">
        select id,name,age,score from student where name like '%${value}%'
    </select>

    3.#{}与${}区别

    • 1.能使用#{}的时候尽量使用#{},不使用${}
    • 2.#{}相当于jdbc中的preparedstatement(预编译),${}是直接使用里面的值进行拼接,如果解释预编译和直接拼接,我想可以这么理解预编译:比如将一个#{name}传进来,预编译是先将sql语句编译成为模板,也就是我知道你要干什么,假设这个sql是要查询名字为xxx的学生信息,那无论这个xxx里面是什么信息,我都只会去根据名字这一列查询,里面无论写的是什么,都只会当做一个字符串,这个类型在预编译的时候已经定义好了。
    • 3.${}就不一样,是将语句拼接之后才确定查询条件/类型的,那么就会有被注入的可能性,有些人故意将名字设置为删除条件,这时候sql就变成删除操作了。
    • 所以我们一般类似模糊查询都是用#{}拼接
    <select id="selectStudentsByName" resultType="Student">
        select id,name,age,score from student where name like '%' #{name} '%'
    </select>
    • 但是对于order by 我们是用不了#{}的,因为用了这个就会被自动转换成字符串,自动加引号,这样语句就不生效了。
    <select id="selectStudentsByName" resultType="Student">
        select id,name,age,score from student order by #{column}
    </select>
    
    <!--编译出来的结果如下:-->
    select * from table order by 'column'

    那我们需要怎么处理呢?我们只能使用${}MyBatis不会修改或转义字符串。这样是不安全的,会导致潜在的SQL注入攻击,我们需要自己限制,不允许用户输入这些字段,或者通常自行转义并检查。所以这必须过滤输入的内容。

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-26

    Lambda【1】-- List相关Lambda表达式使用(上篇)

    Lambda在jdk1.8里面已经很好用了,在这里不讲底层的实现,只有简单的用法,会继续补全。
    首先一个list我们要使用lambda的话,需要使用它的stream()方法,获取流,才能使用后续的方法。

    基础类User.java

    public class User {
    
      public long userId;
    
      public User() {
      }
    
      public User(long userId, String name, int age) {
        this.userId = userId;
        this.name = name;
        this.age = age;
      }
    
      public String name;
      public int age;
    
      public String getName() {
        return name;
      }
    
      public void setName(String name) {
        this.name = name;
      }
    
      public int getAge() {
        return age;
      }
    
      public void setAge(int age) {
        this.age = age;
      }
    
      public long getUserId() {
        return userId;
      }
    
      public void setUserId(long userId) {
        this.userId = userId;
      }
    
      @Override
      public String toString() {
        return "User{" +
            "name='" + name + '\'' +
            ", age=" + age +
            ", userId=" + userId +
            '}';
      }
    
      public void output() {
        System.out.println("User{" +
            "name='" + name + '\'' +
            ", age=" + age +
            ", userId=" + userId +
            '}');
      }
    }

    1.遍历元素

    使用foreach方法,其中s->里面的s指list里面的每一个元素,针对每一个元素都执行后续的方法。如果里面只有一句话,可以直接缩写foreach(n -> System.out.println(n));,如果需要执行的方法里面有两句或者多句需要执行的话,需要可以使用list.stream().forEach(s -> {System.out.println(s);});形式。

      // 遍历list(String)和对象
      public static void foreachListString() {
        List features = Arrays.asList("Lambdas", "Default Method", "Stream API",
            "Date and Time API");
        features.forEach(n -> System.out.println(n));
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        // s代表的是里面的每一个元素,{}里面就是每个元素执行的方法,这个比较容易理解
        list.stream().forEach(s -> {
          System.out.println(s);
        });
    
        // 处理对象
        List<User> users = new ArrayList<>();
        User user1 = new User();
        user1.setAge(1);
        user1.setName("user1");
        user1.setUserId(1);
        users.add(user1);
        users.stream().forEach(s -> s.output());
      }

    2.转化里面的每一个元素

    map是需要返回值的,s代表里面的每一个元素,return 处理后的返回值

    public static void mapList() {
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        List<String> list2 = new ArrayList<>();
        // map代表从一个转成另一个,s代表里面的每一个值,{}代表针对每一个值的处理方法,如果是代码句子,则需要有返回值
        // 返回值代表转化后的值,以下两种都可以
        list2 = list.stream().map(s -> {
          return s.toUpperCase();
        }).collect(Collectors.toList());
        list2.stream().forEach(s -> {
          System.out.println(s);
        });
        list2 = list.stream().map(s -> s.toUpperCase()).collect(Collectors.toList());
        list2.stream().forEach(s -> {
          System.out.println(s);
        });
      }

    3.条件过滤筛选

    使用filter函数,里面的表达式也是需要返回值的,返回值应该为boolean类型,也就是符合条件的就保留在list里面,不符合条件的就被过滤掉。

      // filter过滤
      public static void filterList() {
        List<String> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();
        list1.add("aasd");
        list1.add("agdfs");
        list1.add("bdfh");
        list2 = list1.stream().filter(s -> {
          return s.contains("a");
        }).collect(Collectors.toList());
        list2.stream().forEach(s -> {
          System.out.println(s);
        });
      }

    4.取出list里面的对象中的元素,返回一个特定的list

    这个可以让我们取出list集合中的某一个元素,也是使用map即可。

      // list集合中取出某一属性
      public static void getAttributeList() {
        List<User> list = new ArrayList<>();
        User user1 = new User();
        user1.setUserId(1);
        user1.setName("James");
        user1.setAge(13);
        list.add(user1);
        User user2 = new User();
        user2.setUserId(2);
        user2.setName("Tom");
        user2.setAge(21);
        list.add(user2);
        // 两种书写方式都可以,一个是map里面,使用每一个实例调用User类的getName方法返回值就是转化后的值。
        List<String> tableNames = list.stream().map(User::getName).collect(Collectors.toList());
        tableNames.stream().forEach(s -> {
          System.out.println(s);
        });
        List<String> tableNames1 = list.stream().map(u -> u.getName()).collect(Collectors.toList());
        tableNames1.stream().forEach(s -> {
          System.out.println(s);
        });
      }

    5.分组

    可以根据某一个属性来分组,获得map

      // 分组,每一组都是list
      public static void groupBy() {
        List<User> userList = new ArrayList<>();// 存放user对象集合
        User user1 = new User(1, "张三", 24);
        User user2 = new User(2, "李四", 27);
        User user3 = new User(3, "王五", 21);
        User user4 = new User(4, "张三", 22);
        User user5 = new User(5, "李四", 20);
        User user6 = new User(6, "王五", 28);
        userList.add(user1);
        userList.add(user2);
        userList.add(user3);
        userList.add(user4);
        userList.add(user5);
        userList.add(user6);
        //根据name来将userList分组
        Map<String, List<User>> groupBy = userList.stream().collect(Collectors.groupingBy(User::getName));
        System.out.println(groupBy);
      }

    6.对某一个属性进行求和

    比如我们需要对年龄进行求和,可以使用mapToInt(),里面参数应该使用类名:方法名,最后需要使用sum()来求和。

    public static void getSum(){
        List<User> userList = new ArrayList<>();//存放user对象集合
    
        User user1 = new User(1, "qw", 24);
        User user2 = new User(2, "qwe", 27);
        User user3 = new User(3, "aasf", 21);
        User user4 = new User(4, "fa", 22);
        User user5 = new User(5, "sd", 20);
        User user6 = new User(6, "yr", 28);
    
        userList.add(user1);
        userList.add(user2);
        userList.add(user3);
        userList.add(user4);
        userList.add(user5);
        userList.add(user6);
        // sum()方法,则是对每一个元素进行加和计算
        int totalAge = userList.stream().mapToInt(User::getAge).sum();
        System.out.println("和:" + totalAge);
      }

    7.将list转化成map

    比如我们需要list里面的对象的id和这个对象对应,那就是需要转换成map。需要在collect()方法里面使用Collectors的toMap()方法即可,参数就是key和value。

     public static void listToMap(){
        List<User> userList = new ArrayList<>();
    
        User user1 = new User(1, "12", 22);
        User user2 = new User(2, "21", 17);
        User user3 = new User(3, "a", 11);
        User user4 = new User(4, "a", 22);
        User user5 = new User(5, "af", 22);
        User user6 = new User(6, "fa", 25);
    
        userList.add(user1);
        userList.add(user2);
        userList.add(user3);
        userList.add(user4);
        userList.add(user5);
        userList.add(user6);
    
        Map<Long,User> userMap = userList.stream().collect(Collectors.toMap(User::getUserId, user -> user));
        System.out.println("toMap:" + userMap.toString());
      }

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    技术之路不在一时,山高水长,纵使缓慢,驰而不息。

    公众号:秦怀杂货店

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-26

    Mybatis【8】-- Mybatis返回List或者Map以及模糊查询怎么搞?

    使用mybatis的时候,经常发现一个需求,我怎么知道自己是不是增加/修改/删除数据成功了?

    好像执行sql之后都没有结果的。其实不是的,增删改的sql执行之后都会有一个int类型的返回值,表示的意思是这个操作影响的行数。举个例子,如果我们插入一行成功的话,影响的就是一行。如果我们修改一条数据成功的话,那么我们也是影响了一行。如果我们删除一条数据成功的话,那么返回的就是1,表示影响了一行,如果没有删除任何的数据,那么返回值就是0。所以我们经常使用返回值是否大于0来表示是不是修改(增加/更新/删除都算是一种修改)数据成功。

    比如我们插入数据的时候:

    <insert id="insertStudentCacheId" parameterType="Student">
        insert into student(name,age,score) values(#{name},#{age},#{score})
    </insert>

    接口定义:

    // 增加新学生并返回id返回result
    public int insertStudentCacheId(Student student);

    接口实现:

        public int  insertStudentCacheId(Student student) {
            int result;
            try {
                sqlSession = MyBatisUtils.getSqlSession();
                result =sqlSession.insert("insertStudentCacheId", student);
                sqlSession.commit();
            } finally {
                if (sqlSession != null) {
                    sqlSession.close();
                }
            }
            return result;
        }

    Test方法:

        @Test
        public void testinsertStudentCacheId(){
            Student student=new Student("helloworld",17,101);
            int result = dao.insertStudentCacheId(student);
            System.out.println("result:"+result);
        }

    结果如下:

    这样的方式对于update以及删除方法都是有效的,这是因为他们都是属于修改方法,属于读写模式,而select方式是属于只读方式。

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-26

    Mybatis【7】-- Mybatis如何知道增删改是否成功执行?

    代码直接放在Github仓库【https://github.com/Damaer/Myb...
    需要声明的是:此Mybatis学习笔记,是从原始的Mybatis开始的,而不是整合了其他框架(比如Spring)之后,个人认为,这样能对它的功能,它能帮我们做什么,有更好的理解,后面再慢慢叠加其他的功能。

    我们知道很多时候我们有一个需求,我们需要把插入数据后的id返回来,以便我们下一次操作。

    其实一开始的思路是我插入之后,再执行一次select,根据一个唯一的字段来执行select操作,但是Student这个类如果插入后再根据名字或者年龄查出来,这根本就是不可行的!!!重名与同年龄的人一定不少。
    我们的测试方法如下,我们可以看到插入前是没有值的,插入后就有了值:

    /**
     * 测试插入后获取id
     */
    @Test
    public void testinsertStudentCacheId(){
        Student student=new Student("helloworld",17,85);
        System.out.println("插入前:student="+student);
        dao.insertStudentCacheId(student);
        System.out.println("插入后:student="+student);
    }

    useGeneratedKeys 设置主键自增

        <insert id="insertStudentCacheId" useGeneratedKeys="true" keyProperty="id" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
        </insert>

    需要注意的点:

    • 1.useGeneratedKeys="true"表示设置属性自增
    • 2.keyProperty="id"设置主键的字段
    • 3.parameterType="Student"设置传入的类型
    • 4.注意:虽然有返回类型,但是我们不需要手动设置返回的类型,这个是由框架帮我们实现的,所以对应的接口方法也是没有返回值的,会修改我们插入的对象,设置id值。
    • 5.实体类中id属性字段一定需要set以及get方法

    使用selectKey 查询主键

        <insert id="insertStudentCacheId" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
            <!-- 指定结果类型resultType,keyProperty是属性,自动返回到属性id中,order是次序,after是指获取id是在于插入后 -->
            <selectKey resultType="int" keyProperty="id" order="AFTER">
                select @@identity
            </selectKey>
        </insert>

    或者写成:

        <insert id="insertStudentCacheId" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
            <!-- 指定结果类型resultType,keyProperty是属性,自动返回到属性id中,order是次序,after是指获取id是在于插入后 -->
            <selectKey resultType="int" keyProperty="id" order="AFTER">
                select LAST_INSERT_ID()
            </selectKey>
        </insert>

    两种方式的结果:
    [](http://markdownpicture.oss-cn...

    注意要点:

    • 1.最外层的<insert></insert>没有返回属性(resultType),但是里面的<selectKey></selectKey>是有返回值类型的。
    • 2.order="AFTER"表示先执行插入,之后才执行selectkey语句的。
    • 3.select @@identityselect LAST_INSERT_ID()都表示选出刚刚插入的最后一条数据的id。
    • 4.实体类中id属性字段一定需要set以及get方法
    • 5.此时,接口中仍不需要有返回值,框架会自动将值注入到我们insert的那个对象中,我们可以直接使用就可以了。

    其实,我们的接口中可以有返回值,但是这个返回值不是id,而是表示插入后影响的行数,此时sql中仍和上面一样,不需要写返回值。

    <insert id="insertStudentCacheIdNoReturn" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
            <!-- 指定结果类型resultType,keyProperty是属性,自动返回到属性id中,order是次序,after是指获取id是在于插入后 -->
            <selectKey resultType="int" keyProperty="id" order="AFTER">
                select LAST_INSERT_ID()
            </selectKey>
        </insert>

    接口中:

    // 增加新学生并返回id返回result
    public int insertStudentCacheId(Student student);

    接口的实现类:

    public int  insertStudentCacheId(Student student) {
            int result;
            try {
                sqlSession = MyBatisUtils.getSqlSession();
                result =sqlSession.insert("insertStudentCacheId", student);
                sqlSession.commit();
            } finally {
                if (sqlSession != null) {
                    sqlSession.close();
                }
            }
            return result;
        }

    Test中:

    public void testinsertStudentCacheId(){
            Student student=new Student("helloworld",17,101);
            System.out.println("插入前:student="+student);
            int result = dao.insertStudentCacheId(student);
            System.out.println(result);
            System.out.println("插入后:student="+student);
        }

    结果证明:result的值为1,表示插入了一行,查看数据库,确实插入了数据。

    PS:如果无法创建连接,需要把Mysql的jar包升级:

            <!-- mysql驱动包 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.21</version>
            </dependency>

    如果报以下的错误,那么需要将&改成转义后的符号&amp;

    org.apache.ibatis.exceptions.PersistenceException: 
    ### Error building SqlSession.
    ### Cause: org.apache.ibatis.builder.BuilderException: Error creating document instance.  Cause: org.xml.sax.SAXParseException; lineNumber: 14; columnNumber: 107; 对实体 "serverTimezone" 的引用必须以 ';' 分隔符结尾。

    在xml里面配置需要转义,不在xml文件里面配置则不需要

    <property name="url" value="jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&amp;serverTimezone=UTC"/>

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-05

    Mybatis【6】-- Mybatis插入数据后自增id怎么获取?

    代码直接放在Github仓库【https://github.com/Damaer/Myb...
    需要声明的是:此Mybatis学习笔记,是从原始的Mybatis开始的,而不是整合了其他框架(比如Spring)之后,个人认为,这样能对它的功能,它能帮我们做什么,有更好的理解,后面再慢慢叠加其他的功能。

    我们知道很多时候我们有一个需求,我们需要把插入数据后的id返回来,以便我们下一次操作。

    其实一开始的思路是我插入之后,再执行一次select,根据一个唯一的字段来执行select操作,但是Student这个类如果插入后再根据名字或者年龄查出来,这根本就是不可行的!!!重名与同年龄的人一定不少。
    我们的测试方法如下,我们可以看到插入前是没有值的,插入后就有了值:

    /**
     * 测试插入后获取id
     */
    @Test
    public void testinsertStudentCacheId(){
        Student student=new Student("helloworld",17,85);
        System.out.println("插入前:student="+student);
        dao.insertStudentCacheId(student);
        System.out.println("插入后:student="+student);
    }

    useGeneratedKeys 设置主键自增

        <insert id="insertStudentCacheId" useGeneratedKeys="true" keyProperty="id" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
        </insert>

    需要注意的点:

    • 1.useGeneratedKeys="true"表示设置属性自增
    • 2.keyProperty="id"设置主键的字段
    • 3.parameterType="Student"设置传入的类型
    • 4.注意:虽然有返回类型,但是我们不需要手动设置返回的类型,这个是由框架帮我们实现的,所以对应的接口方法也是没有返回值的,会修改我们插入的对象,设置id值。
    • 5.实体类中id属性字段一定需要set以及get方法

    使用selectKey 查询主键

        <insert id="insertStudentCacheId" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
            <!-- 指定结果类型resultType,keyProperty是属性,自动返回到属性id中,order是次序,after是指获取id是在于插入后 -->
            <selectKey resultType="int" keyProperty="id" order="AFTER">
                select @@identity
            </selectKey>
        </insert>

    或者写成:

        <insert id="insertStudentCacheId" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
            <!-- 指定结果类型resultType,keyProperty是属性,自动返回到属性id中,order是次序,after是指获取id是在于插入后 -->
            <selectKey resultType="int" keyProperty="id" order="AFTER">
                select LAST_INSERT_ID()
            </selectKey>
        </insert>

    两种方式的结果:
    [](http://markdownpicture.oss-cn...

    注意要点:

    • 1.最外层的<insert></insert>没有返回属性(resultType),但是里面的<selectKey></selectKey>是有返回值类型的。
    • 2.order="AFTER"表示先执行插入,之后才执行selectkey语句的。
    • 3.select @@identityselect LAST_INSERT_ID()都表示选出刚刚插入的最后一条数据的id。
    • 4.实体类中id属性字段一定需要set以及get方法
    • 5.此时,接口中仍不需要有返回值,框架会自动将值注入到我们insert的那个对象中,我们可以直接使用就可以了。

    其实,我们的接口中可以有返回值,但是这个返回值不是id,而是表示插入后影响的行数,此时sql中仍和上面一样,不需要写返回值。

    <insert id="insertStudentCacheIdNoReturn" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
            <!-- 指定结果类型resultType,keyProperty是属性,自动返回到属性id中,order是次序,after是指获取id是在于插入后 -->
            <selectKey resultType="int" keyProperty="id" order="AFTER">
                select LAST_INSERT_ID()
            </selectKey>
        </insert>

    接口中:

    // 增加新学生并返回id返回result
    public int insertStudentCacheId(Student student);

    接口的实现类:

    public int  insertStudentCacheId(Student student) {
            int result;
            try {
                sqlSession = MyBatisUtils.getSqlSession();
                result =sqlSession.insert("insertStudentCacheId", student);
                sqlSession.commit();
            } finally {
                if (sqlSession != null) {
                    sqlSession.close();
                }
            }
            return result;
        }

    Test中:

    public void testinsertStudentCacheId(){
            Student student=new Student("helloworld",17,101);
            System.out.println("插入前:student="+student);
            int result = dao.insertStudentCacheId(student);
            System.out.println(result);
            System.out.println("插入后:student="+student);
        }

    结果证明:result的值为1,表示插入了一行,查看数据库,确实插入了数据。

    PS:如果无法创建连接,需要把Mysql的jar包升级:

            <!-- mysql驱动包 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.21</version>
            </dependency>

    如果报以下的错误,那么需要将&改成转义后的符号&amp;

    org.apache.ibatis.exceptions.PersistenceException: 
    ### Error building SqlSession.
    ### Cause: org.apache.ibatis.builder.BuilderException: Error creating document instance.  Cause: org.xml.sax.SAXParseException; lineNumber: 14; columnNumber: 107; 对实体 "serverTimezone" 的引用必须以 ';' 分隔符结尾。

    在xml里面配置需要转义,不在xml文件里面配置则不需要

    <property name="url" value="jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&amp;serverTimezone=UTC"/>

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-05

    Mybatis【5】-- Mybatis多种增删改查那些你会了么?

    前面我们学会了Mybatis如何配置数据库以及创建SqlSession,那怎么写呢?crud怎么写?

    代码直接放在Github仓库【https://github.com/Damaer/Myb...
    需要声明的是:此Mybatis学习笔记,是从原始的Mybatis开始的,而不是整合了其他框架(比如Spring)之后,个人认为,这样能对它的功能,它能帮我们做什么,有更好的理解,后面再慢慢叠加其他的功能。

    项目的目录如下:

    创建数据库:初始化数据,SQL语句如下(也就是resource下的test.sql

    #创建数据库
    CREATE DATABASE `test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
    #创建数据表
    CREATE TABLE `student` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(20) NOT NULL ,
    `age` INT NOT NULL , `score` DOUBLE NOT NULL , PRIMARY KEY (`id`)) ENGINE = MyISAM;

    使用maven管理项目,pom.xml文件管理依赖jar包:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.test</groupId>
        <artifactId>test</artifactId>
        <version>1.0-SNAPSHOT</version>
        <dependencies>
            <!-- mybatis核心包 -->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.3.0</version>
            </dependency>
            <!-- mysql驱动包 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.29</version>
            </dependency>
            <!-- junit测试包 -->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.11</version>
                <scope>test</scope>
            </dependency>
            <!-- 日志文件管理包 -->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.12</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
                <version>1.7.12</version>
            </dependency>
        </dependencies>
    </project>

    与数据库中相对应的实体类Student:

    package bean;
    
    public class Student {
        private Integer id;
        private String name;
        private int age;
        private double score;
        public Student(){
    
        }
        public Student(String name, int age, double score) {
            super();
            this.name = name;
            this.age = age;
            this.score = score;
        }
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public double getScore() {
            return score;
        }
        public void setScore(double score) {
            this.score = score;
        }
        @Override
        public String toString() {
            return "Student [id=" + id + ", name=" + name + ", age=" + age
                    + ", score=" + score + "]";
        }
        
    }

    使用mybatis的重要一步是配置数据源,要不怎么知道使用哪一个数据库,有哪些mapper文件,主配置文件 mybatis.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <!-- 配置数据库文件 -->
        <properties resource="jdbc_mysql.properties">
    
        </properties>
        <!-- 别名,对数据对象操作全名太长,需要使用别名 -->
        <typeAliases>
            <!--<typeAlias type="bean.Student" alias="Student"/>-->
            <!--直接使用类名即可,对于整个包的路径配置(别名),简单快捷 -->
            <package name="bean"/>
        </typeAliases>
        <!-- 配置运行环境 -->
        <!-- default 表示默认使用哪一个环境,可以配置多个,比如开发时的测试环境,上线后的正式环境等 -->
        <environments default="mysqlEM">
            <environment id="mysqlEM">
                <transactionManager type="JDBC">
                </transactionManager>
                <dataSource type="POOLED">
                    <property name="driver" value="${jdbc.driver}"/>
                    <property name="url" value="${jdbc.url}"/>
                    <property name="username" value="${jdbc.user}"/>
                    <property name="password" value="${jdbc.password}"/>
                </dataSource>
            </environment>
        </environments>
        <!-- 注册映射文件 -->
        <mappers>
            <mapper resource="mapper/mapper.xml"/>
        </mappers>
    </configuration>

    mybatis.xml文件里面抽取出来的数据库连接相关信息jdbc_mysql.properties文件:

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/test
    jdbc.user=root
    jdbc.password=123456

    日志系统的配置文件 log4j.properties:

    log4j.prpp
    log4j.rootLogger=DEBUG, stdout
    
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=[service] %d - %c -%-4r [%t] %-5p %c %x - %m%n
    
    #log4j.appender.R=org.apache.log4j.DailyRollingFileAppender    
    #log4j.appender.R.File=../logs/service.log    
    #log4j.appender.R.layout=org.apache.log4j.PatternLayout    
    #log4j.appender.R.layout.ConversionPattern=[service] %d - %c -%-4r [%t] %-5p %c %x - %m%n    
    
    #log4j.logger.com.ibatis = debug    
    #log4j.logger.com.ibatis.common.jdbc.SimpleDataSource = debug    
    #log4j.logger.com.ibatis.common.jdbc.ScriptRunner = debug    
    #log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate = debug    
    #log4j.logger.java.sql.Connection = debug    
    log4j.logger.java.sql.Statement = debug
    log4j.logger.java.sql.PreparedStatement = debug
    log4j.logger.java.sql.ResultSet =debug

    在主配置文件mybatis.xml里我们配置了去扫描mapper文件,那我们要实现的是对Student的增删改查等功能,Mapper.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="abc">
        <!-- parameterType可以省略不写 -->
        <insert id="insertStudent" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
        </insert>
    
        <insert id="insertStudentCacheId" parameterType="Student">
            insert into student(name,age,score) values(#{name},#{age},#{score})
            <!-- 指定结果类型resultType,keyProperty是属性,自动返回到属性id中,order是次序,after是指获取id是在于插入后 -->
            <selectKey resultType="int" keyProperty="id" order="AFTER">
                select @@identity
            </selectKey>
        </insert>
    
        <!-- 删除 -->
        <delete id="deleteStudentById" >
            delete from student where id=#{id}
            <!-- 这里的id放什么都可以,只是一个占位符,不表示什么 -->
        </delete>
    
        <update id="updateStudent">
            update student set name=#{name},age=#{age},score=#{score} where id=#{id}
        </update>
    
        <!-- 查询列表 -->
        <!-- 系统不知道返回封装为什么类型,所以要注明返回类型 -->
        <select id="selectAllStudents" resultType="Student">
            select id,name,age,score from student
            <!-- 如果数据库为tid,tname,tage,那么我们可以使用别名
            select tid id,tname name,tage age,tscore score from student -->
        </select>
        <!-- 通过id来查询学生 -->
        <select id="selectStudentById" resultType="Student">
            select * from student where id=#{xxx}
    
        </select>
        <!-- 模糊查询-->
        <!-- 不能写成'%#{name}%' -->
        <!-- 可以写成这样,也就是使函数拼接 select id,name,age,score from student where name like concat('%',#{xxx},'%') -->
        <!-- 也可以写成这样,‘’引起来的是写死的,而变量是不可以引起来的select id,name,age,score from student where name like '%' #{xxx} '%' -->
        <!-- '%' #{xxx} '%'中间必须有空格,要不就无效了 -->
        <select id="selectStudentsByName" resultType="Student">
            <!--最常用的(动态参数) select id,name,age,score from student where name like '%' #{name} '%' -->
            <!-- 下面的是字符串拼接 ,只能写value,了解即可,容易sql注入,执行效率低,不建议使用-->
            select id,name,age,score from student where name like '%${value}%'
        </select>
    </mapper>

    有了mapper.xml文件还不够,我们需要定义接口与sql语句一一对应,IStudentDao.class

    package dao;
    import bean.Student;
    import java.util.List;
    import java.util.Map;
    public interface IStudentDao {
        // 增加学生
        public void insertStudent(Student student);
        // 增加新学生并返回id
        public void insertStudentCacheId(Student student);
    
        // 根据id删除学生
        public void deleteStudentById(int id);
        // 更新学生的信息
        public void updateStudent(Student student);
    
        // 返回所有学生的信息List
        public List<Student> selectAllStudents();
        // 返回所有学生的信息Map
        public Map<String, Object> selectAllStudentsMap();
    
        // 根据id查找学生
        public Student selectStudentById(int id);
        // 根据名字查找学生,模糊查询
        public List<Student>selectStudentsByName(String name);
    }

    接口的实现类如下:
    sqlSession有很多方法:

    • 如果是插入一条数据需要使用insert();
    • 删除一条数据使用delete();
    • 更新一条数据使用update();
    • 如果查询返回数据的List使用SelectList()方法;
    • 如果返回查询多条数据的Map使用selectMap();
    • 如果查询一条数据,那么只需要使用selectOne()即可。
    package dao;
    
    import bean.Student;
    import org.apache.ibatis.session.SqlSession;
    import utils.MyBatisUtils;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class StudentDaoImpl implements IStudentDao {
        private SqlSession sqlSession;
        public void insertStudent(Student student) {
            //加载主配置文件
            try {
                sqlSession = MyBatisUtils.getSqlSession();
                sqlSession.insert("insertStudent", student);
                sqlSession.commit();
            } finally {
                if (sqlSession != null) {
                    sqlSession.close();
                }
            }
        }
    
        public void insertStudentCacheId(Student student) {
            try {
                sqlSession = MyBatisUtils.getSqlSession();
                sqlSession.insert("insertStudentCacheId", student);
                sqlSession.commit();
            } finally {
                if (sqlSession != null) {
                    sqlSession.close();
                }
            }
        }
    
        public void deleteStudentById(int id) {
            try {
                sqlSession = MyBatisUtils.getSqlSession();
                sqlSession.delete("deleteStudentById", id);
                sqlSession.commit();
            } finally {
                if (sqlSession != null) {
                    sqlSession.close();
                }
            }
        }
    
        public void updateStudent(Student student) {
            try {
                sqlSession = MyBatisUtils.getSqlSession();
                sqlSession.update("updateStudent", student);
                sqlSession.commit();
            } finally {
                if (sqlSession != null) {
                    sqlSession.close();
                }
            }
        }
    
        public List<Student> selectAllStudents() {
            List<Student> students ;
            try {
                sqlSession = MyBatisUtils.getSqlSession();
                students = sqlSession.selectList("selectAllStudents");
                //查询不用修改,所以不用提交事务
            } finally {
                if (sqlSession != null) {
                    sqlSession.close();
                }
            }
            return students;
        }
    
        public Map<String, Object> selectAllStudentsMap() {
            Map<String ,Object> map=new HashMap<String, Object>();
            /**
             * 可以写成Map<String ,Student> map=new HashMap<String, Student>();
             */
            try {
                sqlSession=MyBatisUtils.getSqlSession();
                map=sqlSession.selectMap("selectAllStudents", "name");
                //查询不用修改,所以不用提交事务
            } finally{
                if(sqlSession!=null){
                    sqlSession.close();
                }
            }
            return map;
        }
    
        public Student selectStudentById(int id) {
            Student student=null;
            try {
                sqlSession=MyBatisUtils.getSqlSession();
                student=sqlSession.selectOne("selectStudentById",id);
                sqlSession.commit();
            } finally{
                if(sqlSession!=null){
                    sqlSession.close();
                }
            }
            return student;
        }
    
        public List<Student> selectStudentsByName(String name) {
            List<Student>students=new ArrayList<Student>();
            try {
                sqlSession=MyBatisUtils.getSqlSession();
                students=sqlSession.selectList("selectStudentsByName",name);
                //查询不用修改,所以不用提交事务
            } finally{
                if(sqlSession!=null){
                    sqlSession.close();
                }
            }
            return students;
        }
    }

    我们使用了一个自己定义的工具类,用来获取Sqlsession的实例:

    package utils;
    
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    public class MyBatisUtils {
        static private SqlSessionFactory sqlSessionFactory;
    
        static public SqlSession getSqlSession() {
            InputStream is;
            try {
                is = Resources.getResourceAsStream("mybatis.xml");
                if (sqlSessionFactory == null) {
                    sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
                }
                return sqlSessionFactory.openSession();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return null;
        }
    }
    

    测试代码MyTest.class:

    import bean.Student;
    import dao.IStudentDao;
    import dao.StudentDaoImpl;
    import org.junit.Before;
    import org.junit.Test;
    import java.util.List;
    import java.util.Map;
    
    public class MyTest {
        private IStudentDao dao;
        @Before
        public void Before(){
            dao=new StudentDaoImpl();
        }
        /**
         * 插入测试
         */
        @Test
        public void testInsert(){
            /**
             * 要是没有select id,这样是不会自动获取id的,id会一直为空
             */
            Student student=new Student("hello",14,94.6);
            System.out.println("插入前:student="+student);
            dao.insertStudent(student);
            System.out.println("插入后:student="+student);
        }
        /**
         * 测试插入后获取id
         */
        @Test
        public void testinsertStudentCacheId(){
            Student student=new Student("helloworld",17,85);
            System.out.println("插入前:student="+student);
            dao.insertStudentCacheId(student);
            System.out.println("插入后:student="+student);
        }
        /*
         * 测试删除
         *
         */
        @Test
        public void testdeleteStudentById(){
            dao.deleteStudentById(18);
    
        }
        /*
         * 测试修改,一般我们业务里面不这样写,一般是查询出来student再修改
         *
         */
        @Test
        public void testUpdate(){
            Student student=new Student("lallalalla",14,94.6);
            student.setId(21);
            dao.updateStudent(student);
    
        }
        /*
         * 查询列表
         *
         */
        @Test
        public void testselectList(){
            List<Student> students=dao.selectAllStudents();
            if(students.size()>0){
                for(Student student:students){
                    System.out.println(student);
                }
            }
        }
        /*
         * 查询列表装成map
         *
         */
        @Test
        public void testselectMap(){
            Map<String,Object> students=dao.selectAllStudentsMap();
            // 有相同的名字的会直接替换掉之前查出来的,因为是同一个key
            System.out.println(students.get("helloworld"));
            System.out.println(students.get("1ADAS"));
        }
        /*
         * 通过id来查询student
         *
         */
        @Test
        public void testselectStudentById(){
            Student student=dao.selectStudentById(19);
            System.out.println(student);
        }
        /*
         * 通过模糊查询student的名字
         *
         */
        @Test
        public void testselectStudentByName(){
            List<Student>students=dao.selectStudentsByName("abc");
            if(students.size()>0){
                for(Student student:students)
                    System.out.println(student);
            }
    
        }
    }
    

    至此这个demo就完成了,运行test的时候建议多跑几次插入再测其他功能。

    从上面的代码我们可以看出Mybatis总体运行的逻辑:

    • 1.通过加载mybatis.xml文件,然后解析文件,获取数据库连接信息,存起来。
    • 2.扫描mybatis.xml里面配置的mapper.xml文件。
    • 3.扫描mapper.xml文件的时候,,将sql按照namespaceid存起来。
    • 4.通过刚刚存起来的数据库连接信息,build出一个sqlSessionFactory工厂,sqlSessionFactory又可以获取到openSession,相当于获取到数据库会话。
    • 5.通过SqlSessioninsert()update(),delete()等方法,里面传入id和参数,就可以查找到刚刚扫描mapper.xml文件时存起来的sql,去执行sql。

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-05

    JDBC【4】-- jdbc预编译与拼接sql对比

    • 在jdbc中,有三种方式执行sql,分别是使用Statement(sql拼接),PreparedStatement(预编译),还有一种CallableStatement(存储过程),在这里我就不介绍CallableStatement了,我们来看看Statement与PreparedStatement的区别。
    1. 创建数据库,数据表

    数据库名字是test,数据表的名字是student,里面有四个字段,一个是id,也就是主键(自动递增),还有名字,年龄,成绩。最后先使用sql语句插入六个测试记录。

    CREATE DATABASE `test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
    CREATE TABLE `student` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(20) NOT NULL , 
    `age` INT NOT NULL , `score` DOUBLE NOT NULL , PRIMARY KEY (`id`)) ENGINE = MyISAM; 
    INSERT INTO `student` VALUES (1, '小红', 26, 83);
    INSERT INTO `student` VALUES (2, '小白', 23, 93);
    INSERT INTO `student` VALUES (3, '小明', 34, 45);
    INSERT INTO `student` VALUES (4, '张三', 12, 78);
    INSERT INTO `student` VALUES (5, '李四', 33, 96);
    INSERT INTO `student` VALUES (6, '魏红', 23, 46);

    建立对应的学生类:

    /**
     * student类,字段包括id,name,age,score
     * 实现无参构造,带参构造,toString方法,以及get,set方法
     * @author 秦怀
     */
    public class Student {
        private int id;
        private String name;
        private int age;
        private double score;
        
        public Student() {
            super();
            // TODO Auto-generated constructor stub
        }
        public Student(String name, int age, double score) {
            super();
            this.name = name;
            this.age = age;
            this.score = score;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public double getScore() {
            return score;
        }
        public void setScore(double score) {
            this.score = score;
        }
        @Override
        public String toString() {
            return "Student [id=" + id + ", name=" + name + ", age=" + age
                    + ", score=" + score + "]";
        }
        
    }
    
    2.Statement

    先来看代码,下面是获取数据库连接的工具类 DBUtil.class

    public class DBUtil {
        private static String URL="jdbc:mysql://127.0.0.1:3306/test";
        private static String USER="root";
        private static String PASSWROD ="123456";
        private static Connection connection=null;
        static{
            try {
                Class.forName("com.mysql.jdbc.Driver");
                // 获取数据库连接
                connection=DriverManager.getConnection(URL,USER,PASSWROD);
                System.out.println("连接成功");
            } catch (ClassNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (SQLException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        // 返回数据库连接
        public static Connection getConnection(){
            return connection;
        }
    }

    下面是根据id查询学生信息的代码片段,返回student对象就能输出了:

        public Student selectStudentByStatement(int id){
            // 拼接sql语句
            String sql ="select * from student where id = "+id;
            try {
                // 获取statement对象
                Statement statement = DBUtil.getConnection().createStatement();
                // 执行sql语句,返回 ResultSet
                ResultSet resultSet = statement.executeQuery(sql);
                Student student = new Student();
                // 一条也只能使用resultset来接收
                while(resultSet.next()){
                    student.setId(resultSet.getInt("id"));
                    student.setName(resultSet.getString("name"));
                    student.setAge(resultSet.getInt("age"));
                    student.setScore(resultSet.getDouble("score"));
                }
                return student;
            } catch (SQLException e) {
                // TODO: handle exception
            }
            return null;
        }
    

    我们可以看到整个流程是先获取到数据库的连接Class.forName("com.mysql.jdbc.Driver"); connection=DriverManager.getConnection(URL,USER,PASSWROD);获取到连接之后通过连接获取statement对象,通过statement来执行sql语句,返回resultset这个结果集,Statement statement = DBUtil.getConnection().createStatement();ResultSet resultSet = statement.executeQuery(sql);,值得注意的是,上面的sql是已经拼接好,写固定了的sql,所以很容易被注入,比如这句:

    sql = "select * from user where name= '" + name + "' and password= '" + password+"'";

    如果有人

    • name = "name' or '1'= `1"
    • password = "password' or '1'='1",那么整个语句就会变成:
    sql = "select * from user where name= 'name' or '1'='1' and password= 'password' or '1'='1'";

    那么就会返回所有的信息,所以这是很危险的。
    还有更加危险的,是在后面加上删除表格的操作,不过一般我们都不会把这些权限开放的。

    // 如果password = " ';drop table user;select * from user where '1'= '1"
    // 后面一句不会执行,但是这已经可以删除表格了
    sql = "select * from user where name= 'name' or '1'='1' and password= '' ;drop table user;select * from user where '1'= '1'";

    所以预编译显得尤为重要了。

    3.PreparedStatement预编译

    我们先来看看预编译的代码:

        // 根据id查询学生
        public Student selectStudent(int id){
            String sql ="select * from student where id =?";
            try {
                PreparedStatement preparedStatement = DBUtil.getConnection()..prepareStatement(sql);
                preparedStatement.setInt(1, id);
                ResultSet resultSet = preparedStatement.executeQuery();
                Student student = new Student();
                // 一条也只能使用resultset来接收
                while(resultSet.next()){
                    student.setId(resultSet.getInt("id"));
                    student.setName(resultSet.getString("name"));
                    student.setAge(resultSet.getInt("age"));
                    student.setScore(resultSet.getDouble("score"));
                }
                return student;
            } catch (SQLException e) {
                // TODO: handle exception
            }
            return null;
        }

    预编译也是同样需要获取到数据库连接对象connection,但是sql语句拼接的时候使用了占位符?,将含有占位符的sql当参数传进去,获取到PreparedStatement预编译的对象,最后是通过set来绑定参数,然后再去使用execute执行预编译过的代码。这样就避免了sql注入的问题,同时,由于sql已经编译过缓存在数据库中,所以执行起来不用再编译,速度就会比较快。

    4.为什么预编译可以防止sql注入
    • 在使用占位符,或者说参数的时候,数据库已经将sql指令编译过,那么查询的格式已经订好了,也就是我们说的我已经明白你要做什么了,你要是将不合法的参数传进去,会有合法性检查,用户只需要提供参数给我,参数不会当成指令部分来执行,也就是预编译已经把指令以及参数部分区分开,参数部分不允许传指令进来。

    这样的好处查询速度提高,因为有了预编译缓存,方便维护,可读性增强,不会有很多单引号双引号,容易出错,防止大部分的sql注入,因为参数和sql指令部分数据库系统已经区分开。百度文库里面提到:传递给PreparedStatement对象的参数可以被强制进行类型转换,使开发人员可以确保在插入或查询数据时与底层的数据库格式匹配。
    要是理解不透彻可以这么来理解:

    select * from student where name= ?

    预编译的时候是先把这句话编译了,生成sql模板,相当于生成了一个我知道你要查名字了,你把名字传给我,你现在想耍点小聪明,把字符串'Jame' or '1=1'传进去,你以为他会变成下面这样么:

    select * from student where name= 'Jame' or '1=1'

    放心吧,不可能的,这辈子都不可能的啦,数据库都知道你要干嘛了,我不是有sql模板了么,数据库的心里想的是我叫你传名字给我,行,这名字有点长,想害我,可以,我帮你找,那么数据库去名字这一字段帮你找一个叫'Jame' or '1=1'的人,他心里想这人真逗,没有这个人,没有!!!
    所以这也就是为什么预编译可以防止sql注入的解释了,它是经过了解释器解释过的,解释的过程我就不啰嗦了,只要是对参数做转义,转义之后让它在拼接时只能表示字符串,不能变成查询语句。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    技术之路不在一时,山高水长,纵使缓慢,驰而不息。

    公众号:秦怀杂货店

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-05

    【java基础】-- java接口和抽象类的异同分析

    • 在java中,通常初学者搞不懂接口与抽象类,这也是面试比较容易问到的一个问题。下面我来谈谈自己的理解。如有不妥之处,还望批评指正,不胜感激。

    [TOC]

    1.抽象类怎么定义和继承?

    我们定义一个抽象类person.class表示类(人):

    //使用关键字abstract
    public abstract class person {
        //吃东西的抽象方法,已经有所实现
        public void eat(){
            System.out.println("我是抽象方法吃东西");
        }
        
        //public 修饰的空实现的方法
        public void run(){}
        
        //无修饰,空实现
        void walk(){}
        
        //protected修饰的方法,空实现
        protected void sleep(){}
        
        //private修饰的空实现方法
        private void read(){}
    }
    
    • 1.抽象类使用abstract修饰,可以有抽象方法,也可以完全没有抽象方法,也可以是实现了的方法,但是所有的方法必须实现,空实现(public void walk(){})也是实现的一种,而不能写 public void eat(),后面必须带大括号。
    • 2.方法修饰符可以使public,protected,private,或者是没有,没有默认为只能在同一个包下面继承,如果是private那么子类继承的时候就无法继承这个方法,也没有办法进行修改.
    • 下面我们来写一个Teacher.class继承抽象类

    同一个包下继承:

    不同的包下面继承:

    同个包下正确的代码如下(不重写私有的方法):

    public class teacher extends person {
        @Override
        public void run(){
            System.out.println("我是实体类的方法跑步");
        }
        @Override
        void walk(){
            System.out.println("我是实体类的方法走路");
        }
        @Override
        protected void sleep(){
            System.out.println("我是实体类的方法睡觉");
        }
    }
    
    • 结果如下(没有覆盖抽象类吃东西的方法,所以会调用抽象类默认的):

    • 下面代码是重写了eat()方法的代码,重写是即使没有使用@Override也是起作用的:
    public class teacher extends person {
        public void eat(){
            System.out.println("我是实体类的方法吃东西");
        }
        @Override
        public void run(){
            System.out.println("我是实体类的方法跑步");
        }
        @Override
        void walk(){
            System.out.println("我是实体类的方法走路");
        }
        @Override
        protected void sleep(){
            System.out.println("我是实体类的方法睡觉");
        }
    }
    • 结果如下,吃东西的方法被覆盖掉了:

    • 抽象类不能被实例化,比如:

    • 子类可以实现抽象类的方法,也可以不实现,也可以只实现一部分,这样跑起来都是没有问题的,不实现的话,调用是默认使用抽象类的空实现,也就是什么都没有输出,要是抽象类有实现,那么会输出抽象类默认方法。

    比如:

    • 抽象类中可以有具体的方法以及属性(成员变量)
    • 抽象类和普通类之间有很多相同的地方,比如他们都可以都静态成员与静态代码块等等。

    2.接口怎么定义和实现?

    • 接口就是对方法或者动作的抽象,比如person.class想要成为教师,可以实现教师的接口,可以理解为增加能力。
    • 接口不允许定义没有初始化的属性变量,可以定义public static final int i=5;,以及public int number =0;,但不允许public int num;这样定义,所有private的变量都不允许出现,下面是图片

    定义public int number =0;默认是final修饰的,所以也不能改变它的值:

    下面是正确的接口代码:Teacher.java

    public interface Teacher {
        public static final int i=5;
        public int number =0;
        public void teach();
        void study();
    }
    • 实现类TeacherClass.java
    public class TeacherClass implements Teacher{
        @Override
        public void teach() {
            System.out.println("我是一名老师,我要教书");
            System.out.println("接口的static int是:"+i);
        }
    
        @Override
        public void study() {
            System.out.println("我是一名老师,我也要学习");
            System.out.println("接口的int number是:"+number);
        }
    }
    • 测试类Test.java
    public class Test {
        public static void main(String[] args){
            TeacherClass teacherClass = new TeacherClass();
            teacherClass.study();
            teacherClass.teach();
            System.out.println("-----------------------------------------------------");
            Teacher teacher =teacherClass;
            teacher.study();
            teacher.teach();
        }
    }

    结果:

    分析:接口里面所定义的成员变量都是final的,不可变的,实现接口必须实现接口里面所有的方法,不能只实现一部分,没有使用static final修饰的,默认也是final,同时必须有初始化的值,接口不能直接创建对象,比如Teacher teacher = new Teacher() ,但是可以先创建一个接口的实现类,然后再赋值于接口对象。

    3.总结与对比

    抽象类接口
    使用关键字abstract修饰使用关键字interface
    使用关键字extends实现继承,可以只实现一部分方法,一部分不实现,或者不实现也可以implements来实现接口,实现接口必须实现里面都有的方法
    抽象类里面的方法可以是空实现,可以默认实现,但是必须要带{}接口里面的方法都没有实现体,也就是{}
    抽象类中可以有具体的方法以及属性,也可以有静态代码块,静态成员变量接口里面不能有普通成员变量,必须都是不可变的final成员变量,而且所有的成员变量都必须是public
    抽象类里面的方法可以是public,protect,private,但是private无法继承,所以很少人会这么写,如果没有修饰符,那么只能是同一个包下面的类才能继承接口的方法只能是public或者无修饰符,所有的private修饰都是会报错的
    如果有改动,添加新的方法,可以直接在抽象类中实现默认的即可,也可以在实现类中实现接口增加新方法必须在接口中声明,然后在实现类中进行实现
    抽象类不能直接创建对象接口也不能直接创建对象 ,可以赋予实现类的对象
    抽象类可以有main方法,而且我们可以直接运行,抽象类也可以有构造器接口不能有main方法,接口不能有构造器

    那么我们什么时候使用接口什么时候使用抽象类呢?

    • java有一个缺点,只能实现单继承,个人觉得接口是为了弥补单继承而设计的。
    • 接口是对本质的抽象,比如人,可以设计为person.class这个抽象类,提供相关的方法,属性,但是接口是只提供方法的,也就是像增加功能的,那么也就是对方法的抽象。
    • 如果需要默认实现,或者基本功能不断改变,那么建议使用抽象类,如果只是增加一种方法,那么建议使用接口,如果想实现多重继承,只能是接口与抽象类一起使用以达到想要实现的功能。

    本文章是初学时的记录,仅是初级的对比,深入学习还需各位保持Keep going~

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    技术之路不在一时,山高水长,纵使缓慢,驰而不息。Keep going~

    公众号:秦怀杂货店

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-05

    【Java基础】-- instanceof 用法详解

    1. instanceof关键字

    如果你之前一直没有怎么仔细了解过instanceof关键字,现在就来了解一下:
    image

    instanceof其实是java的一个二元操作符,和=,<,>这些是类似的,同时它也是被保留的关键字,主要的作用,是为了测试左边的对象,是不是右边的类的实例,返回的是boolean值。

    A instanceof B

    注意:A是实例,而B则是Class类

    下面使用代码测试一下:

    class A{
    }
    interface InterfaceA{
    
    }
    class B extends A implements InterfaceA{
    
    }
    public class Test {
        public static void main(String[] args) {
            B b = new B();
            System.out.println(b instanceof B);
            System.out.println(b instanceof A);
            System.out.println(b instanceof InterfaceA);
            
            A a = new A();
            System.out.println(a instanceof InterfaceA);
        }
    }

    输出结果如下:

    true
    true
    true
    false

    从上面的结果,其实我们可以看出instanceof,相当于判断当前对象能不能装换成为该类型,java里面上转型是安全的,子类对象可以转换成为父类对象,接口实现类对象可以装换成为接口对象。

    对象aInterface没有什么关系,所以返回false

    那如果我们装换成为Object了,它还能认出来是哪一个类的对象么?

    public class Test {
        public static void main(String[] args) {
            Object o = new ArrayList<Integer>();
            System.out.println(o instanceof ArrayList);
    
            String str = "hello world";
            System.out.println(str instanceof String);
            System.out.println(str instanceof Object);
        }
    }

    上面的结果返回都是true,也就是认出来还是哪一个类的对象。同时我们使用String对象测试的时候,发现对象既是String的实例,也是Object的实例,也印证了Java里面所有类型都默认继承了Obejct

    但是值得注意的是,我们只能使用对象来instanceof,不能使用基本数据类型,否则会报错。

    如果对象为null,那是什么类型?

    这个答案是:不知道什么类型,因为null可以转换成为任何类型,所以不属于任何类型,instanceof结果会是false

    具体的实现策略我们可以在官网找到:https://docs.oracle.com/javas...

    如果Sobjectref所引用的对象的类,而T是已解析类,数组或接口的类型,则instanceof确定是否 objectrefT的一个实例。S s = new A(); s instanceof T

    • 如果S是一个普通的(非数组)类,则:

      • 如果T是一个类类型,那么S必须是T的同一个类,或者S必须是T的子类;
      • 如果T是接口类型,那么S必须实现接口T。
    • 如果S是接口类型,则:

      • 如果T是类类型,那么T必须是Object。
      • 如果T是接口类型,那么T一定是与S相同的接口或S的超接口。
    • 如果S是表示数组类型SC的类[],即类型SC的组件数组,则:

      • 如果T是类类型,那么T必须是Object。
      • 如果T是一种接口类型,那么T必须是数组实现的接口之一(JLS§4.10.3)。
      • 如果T是一个类型为TC的数组[],即一个类型为TC的组件数组,那么下列其中一个必须为真:

        • TC和SC是相同的原始类型。
        • TC和SC是引用类型,类型SC可以通过这些运行时规则转换为TC。

    但是具体的底层原理我在知乎找到的R大 回答的相关问题,https://www.zhihu.com/questio...

    2. isInstance()方法

    其实这个和上面那个是基本相同的,主要是这个调用者是Class对象,判断参数里面的对象是不是这个Class对象的实例。

    class A {
    }
    
    interface InterfaceA {
    
    }
    
    class B extends A implements InterfaceA {
    
    }
    
    public class Test {
        public static void main(String[] args) {
            B b = new B();
            System.out.println(B.class.isInstance(b));
            System.out.println(A.class.isInstance(b));
            System.out.println(InterfaceA.class.isInstance(b));
    
            A a = new A();
            System.out.println(InterfaceA.class.isInstance(a));
        }
    }

    历史总是惊人的相似!!!

    true
    true
    true
    false

    事实证明,这个isInstance(o)判断的是o是否属于当前Class类的实例.

    不信?再来测试一下:

    public class Test {
        public static void main(String[] args) {
            String s = "hello";
            System.out.println(String.class.isInstance(s));                 // true
            System.out.println(Object.class.isInstance(s));                 // true
    
            
            System.out.println("=============================");
            Object o = new ArrayList<String>();
            System.out.println(String.class.isInstance(o));                    // false
            System.out.println(ArrayList.class.isInstance(o));            // true
            System.out.println(Object.class.isInstance(o));                    // true
        }
    }

    可以看出,其实就是装换成为Object,之前的类型信息还是会保留着,结果和instance一样,区别是:

    • instanceof :前面是实例对象,后面是类型
    • isInstance:调用者(前面)是类型对象,参数(后面)是实例对象

    但是有一个区别哦😯,isInstance()这个方法,是可以使用在基本类型上的,其实也不算是用在基本类型,而是自动做了装箱操作。看下面👇:

            System.out.println(Integer.class.isInstance(1));

    参数里面的1,其实会被装箱成为new Integer(1),所以这样用不会报错。

    3. instanceof,isInstance,isAssignableFrom区别是什么?

    • instanceof 判断对象和类型之间的关系,是关键字,只能用于对象实例,判断左边的对象是不是右边的类(包括父类)或者接口(包括父类)的实例化。
    • isInstance(Object o):判断对象和类型之间的关系,判断o是不是调用这个方法的class(包括父类)或者接口(包括父类)的实例化。
    • isAssignableFrom:判断的是类和类之间的关系,调用者是否可以由参数中的Class对象转换而来。

    注意:java里面一切皆是对象,所以,class本身也是对象。

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    查看原文

    赞 1 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-05

    使用PicGo存储markdown图片(阿里云或者github)

    PicGo代替极简图床

    之前使用极简床图,但是后来好像挂了,真是一件悲伤的事,最近才发现了一个神器,开源的PicGo,已经有各个平台的版本了。链接如下:https://github.com/Molunerfin... 去下载自己的平台即可。虽然你要是Mac,有iPic也是很ok的。

    下载好之后怎么配置呢?

    配置阿里云

    之前是注册阿里云认证之后就送一个oss存储(免费的),不知道现在还有没有,如果有的话就可以按照这个步骤配置下去。https://homenew.console.aliyu...

    打开oss之后,需要自己建立Bucket(存储空间名),存储区域(比如oss-cn-qingdao),除此之外还需要keyidkeysecret。(一定要打开公共读的权限,要不是访问不了的,但是不要打开公共写!!!)

    点击自己的头像,选择accesskeys,新建一个key就可以了,我是新建了公共的。

    安装PicGo之后,打开,选择阿里云,把上面的东西配置进去就可以了。

    然后选择上传区,就可以上传了,选择不同的markdown之类的,也可以选择剪贴板上传,上传成功之后就会有提示,连接就在粘贴板了,直接去粘贴就可以了。

    配置github

    新建一个仓库,然后记得仓库的名字,点击头像,选择settingsdevelopersettings--》personal access token--》新建一个token,记得勾选上权限,我全部选了,那个token只能查看一次,最好保存一下!!!

    然后打开PicGo,选择Github图床,设置好就可以了。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    技术之路不在一时,山高水长,纵使缓慢,驰而不息。

    公众号:秦怀杂货店

    查看原文

    赞 0 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-12-05

    java集合【10】——— LinkedList源码解析

    1.LinkedList介绍

    我们除了最最常用的ArrayList之外,还有LinkedList,这到底是什么东西?从LinkedList官方文档,我们可以了解到,它其实是实现了ListQueue的双向链表结构,而ArrayList底层则是数组结构。

    下面的讲解基于jdk 1.8:

    继承了AbstractSequentialList,实现了List,Queue,Cloneable,Serializable,既可以当成列表使用,也可以当成队列,堆栈使用。主要特点有:

    • 线程不安全,不同步,如果需要同步需要使用List list = Collections.synchronizedList(new LinkedList());
    • 实现List接口,可以对它进行队列操作
    • 实现Queue接口,可以当成堆栈或者双向队列使用
    • 实现Cloneable接口,可以被克隆,浅拷贝
    • 实现Serializable,可以被序列化和反序列化

    下面是LinkedList的结构,注意:指针结束指向的是node,开始的是prev或者next

    源码定义如下:

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
        }

    2.成员变量

    成员变量相对比较简单,因为不像ArrayList一样,需要使用数组保存元素,LinkedList是靠引用来关联前后节点,所以这里只有大小,第一个节点,最后一个节点,以及序列化的uid。

        // 大小
        transient int size = 0;
        // 第一个节点
        transient Node<E> first;
        // 最后一个节点
        transient Node<E> last;
            // 序列化uid
        private static final long serialVersionUID = 876323262645176354L;

    我们来看看Node到底是何方神圣?
    其实就是内部类,里面的item是真正保存节点的地方,next是下一个节点的引用,prev是上一个节点的引用。这里也体现了LinkedList其实就是双线链表。

    只有一个构造函数,三个参数分别对应三个属性。

        private static class Node<E> {
            // 节点里面的数据
            E item;
            // 下一个节点的引用
            Node<E> next;
            // 上一个节点的引用
            Node<E> prev;
    
            // 节点的构造函数,重写之后,无参数构造器已经被覆盖,三个参数分别对应三个属性
            Node(Node<E> prev, E element, Node<E> next) {
                this.item = element;
                this.next = next;
                this.prev = prev;
            }
        }

    3. 构造函数

    构造函数有两个,一个是无参数构造函数,另一个是初始化集合元素,里面调用的其实是addAll,一看就是将里面所有的元素加入到集合中。

        public LinkedList() {
        }
        public LinkedList(Collection<? extends E> c) {
            this();
            addAll(c);
        }

    4. 常用List方法解析

    4.1 查找相关

    4.1.1 getFirst()

    获取第一个元素:

        public E getFirst() {
            // 保存第一个元素为f,注意是final的,
            final Node<E> f = first;
            if (f == null)
                // 如果没有第一个元素,那么就会抛出异常
                throw new NoSuchElementException();
            // 返回第一个元素的item
            return f.item;
        }

    4.1.2 getLast()

    获取最后一个元素,和获取第一个的原理差不多

        public E getLast() {
            // 保存最后一个元素的引用为l
            final Node<E> l = last;
            // 如果为空,抛出错误
            if (l == null)
                throw new NoSuchElementException();
            // 返回item
            return l.item;
        }

    4.1.3 get(int index)

    通过索引来获取元素,里面是调用了另外一个方法先获取节点,再获取该节点的item,在此之前,做了index安全性校验。

        public E get(int index) {
            checkElementIndex(index);
            return node(index).item;
        }

    在👆上面的代码中调用了通过索引位置查找节点位置的函数,下面我们来分析一下这个函数,由于底层是链表实现的,所以呢?遍历起来不是很方便,就考虑到位运算,如果索引位置在后面一半,就从后往前遍历查找,否则从前往后遍历。

        Node<E> node(int index) {
            // assert isElementIndex(index);
                    // size>>1 表示除以2,相当于index小于size的一半
            if (index < (size >> 1)) {
                  // 从前面开始遍历,取出first节点,因为中间过程引用会变化,所以不可直接操作first
                Node<E> x = first;
                  // 通过循环计数来查找
                for (int i = 0; i < index; i++)
                    x = x.next;
                return x;
            } else {
                  // 取出最后一个元素
                Node<E> x = last;
                  // 从后往前遍历
                for (int i = size - 1; i > index; i--)
                    x = x.prev;
                return x;
            }
        }

    4.1.4 indexOf(Object o)

    查找某一个元素的索引位置,分为两种情况讨论,如果要查找的元素为空,那么就使用==,否则使用equals(),这也从侧面印证了LinedList实际上是可以存储null元素的。使用计数查找:

        public int indexOf(Object o) {
            int index = 0;
              // 如果需要查找null元素
            if (o == null) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null)
                        return index;
                    index++;
                }
            } else {
                  // 查找元素不为空
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item))
                        return index;
                    index++;
                }
            }
            return -1;
        }

    4.1.5 lastIndexOf(Object o)

    和前面的indexOf差不多,区别就是这个是后面开始查找,找到第一个符合的元素。

        public int indexOf(Object o) {
            int index = 0;
              // 查找元素
            if (o == null) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null)
                        return index;
                    index++;
                }
            } else {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item))
                        return index;
                    index++;
                }
            }
            return -1;
        }

    4.2 添加元素

    4.2.1 addFirst(E e)

    将元素e,添加到第一个节点,公有方法是addFirst(),但是其实内部调用是linkFirst(),这是private方法。

        public void addFirst(E e) {
            linkFirst(e);
        }
        private void linkFirst(E e) {
            // 先保存第一个节点
            final Node<E> f = first;
            // 初始化一个新节点,prev是null,next是f(之前的首节点)
            final Node<E> newNode = new Node<>(null, e, f);
            // 更新first为新节点
            first = newNode;
            // 如果之前的第一个节点是空的,那么就说明里面是空的,没有元素
            if (f == null)
                // 最后一个元素也是新加入的元素
                last = newNode;
            else
                // f的prev前置节点的引用更新为新的节点
                f.prev = newNode;
            // 个数增加
            size++;
            // 修改次数增加
            modCount++;
        }

    4.2.2 addLast(E e)

    将元素添加在链表最后,其实内部也是直接调用的private方法linkLast():

        public void addLast(E e) {
            linkLast(e);
        }
        void linkLast(E e) {
            // 保存最后一个节点的引用
            final Node<E> l = last;
            // 初始化一个节点,前置节点指针引用指向之前的最后一个节点,后续节点的引用是null
            final Node<E> newNode = new Node<>(l, e, null);
            // 将最后一个节点更新
            last = newNode;
            // 如果之前的最后一个节点是null,说明链表是空的
            if (l == null)
                // 新节点同时是第一个节点
                first = newNode;
            else
                // 否则之前的最后一个节点的后续节点引用更新为新的节点
                l.next = newNode;
            // 大小+1
            size++;
            // 修改次数+1
            modCount++;
        }

    4.2.3 add(E e)

    增加元素,默认也是在链表的最后添加,完成返回true:

        public boolean add(E e) {
            linkLast(e);
            return true;
        }

    4.2.4 addAll(Collection<? extends E> c)

    往链表里面批量添加元素,里面默认是在最后面批量添加,内部调用的是addAll(int index, Collection<? extends E> c),添加之前会判断索引位置是不是合法的。
    然后查找需要插入的位置的前后节点,循环插入。

        public boolean addAll(Collection<? extends E> c) {
            return addAll(size, c);
        }
    
        public boolean addAll(int index, Collection<? extends E> c) {
            // 检查添加位置
            checkPositionIndex(index);
    
            // 将需要添加的集合转换成为数组
            Object[] a = c.toArray();
            // 获取数组的大小
            int numNew = a.length;
            // 如果数组长度为0,说明没有需要添加的元素,返回false
            if (numNew == 0)
                return false;
    
            // 插入位置的前节点和后续节点
            Node<E> pred, succ;
            // 如果插入位置索引大小等于链表大小,那么就是在最后插入元素
            if (index == size) {
                // 最后插入元素没有后续节点
                succ = null;
                // 前一个节点就是之前的最后一个节点
                pred = last;
            } else {
                // 查找到索引为index 的节点
                succ = node(index);
                // 获取前一个节点
                pred = succ.prev;
            }
            
            // 循环插入节点
            for (Object o : a) {
                @SuppressWarnings("unchecked") E e = (E) o;
                // 初始化新节点,上一个节点是pred
                Node<E> newNode = new Node<>(pred, e, null);
                // 如果前一个节点是null,那么第一个节点就是新的节点
                if (pred == null)
                    first = newNode;
                else
                    // 否则pred的next置为新节点
                    pred.next = newNode;
                pred = newNode;
            }
    
            // 如果插入位置没有后续节点,也就是succ为null
            if (succ == null) {
                // 最后一个节点也就是pred,刚刚插入的新节点
                last = pred;
            } else {
                // 加入所有元素之后的最后一个节点的下一个节点指向succ(后续元素)
                pred.next = succ;
                // 插入位置的后续元素的上一个节点引用指向pred
                succ.prev = pred;
            }
              // 大小改变
            size += numNew;
              // 修改次数增加
            modCount++;
            return true;
        }

    上面的代码调用了node(index),这个在前面查找的时候已经说过了,不再解释。

    4.2.5 addAll(int index, Collection<? extends E> c)

    在指定位置批量插入节点:

        public boolean addAll(int index, Collection<? extends E> c) {
              // 检查索引合法性
            checkPositionIndex(index);
              // 将需要插入的集合转换成为数组
            Object[] a = c.toArray();
              // 数组的长度
            int numNew = a.length;
              // 为0则不需要插入
            if (numNew == 0)
                return false;
              // 插入位置的前节点和后节点
            Node<E> pred, succ;
              // 如果在最后插入
            if (index == size) {
                  // 后节点为空
                succ = null;
                  // 前节点是最后一个
                pred = last;
            } else {
                  // 获取插入位置的后节点
                succ = node(index);
                  // 获取前节点
                pred = succ.prev;
            }
                    
              // 遍历
            for (Object o : a) {
                @SuppressWarnings("unchecked") E e = (E) o;
                  // 初始化节点,前置节点是插入位置的前节点,后续节点为null
                Node<E> newNode = new Node<>(pred, e, null);
                  // 如果插入位置前一个节点是null,说明插入位置是链表首
                if (pred == null)
                      // 首节点就是新插入的节点
                    first = newNode;
                else
                      // 前节点的next指向新节点
                    pred.next = newNode;
                  // 更新插入位置的前一个节点
                pred = newNode;
            }
              // 如果插入位置的后一个节点为空,说明插入位置是链表尾部
            if (succ == null) {
                  // 最后一个元素就是插入的元素
                last = pred;
            } else {
                  // 将插入的最后一个元素next指向succ
                pred.next = succ;
                  // succ的上一个元素指向prev
                succ.prev = pred;
            }
              // 大小更新
            size += numNew;
              // 修改次数改变
            modCount++;
              // 返回成功
            return true;
        }

    4.2.6 add(int index,E element)

    将元素插入在指定位置,先判断索引位置,如果索引位置是最后一个,那么直接调用在最后添加元素函数即可,否则需要调用另外一个函数,在某个元素前面插入:

        public void add(int index, E element) {
              // index校验
            checkPositionIndex(index);
              
              // 索引等于链表大小
            if (index == size)
                  // 直接在最后插入元素
                linkLast(element);
            else
                  // 在某个节点前插入元素
                linkBefore(element, node(index));
        }

    4.3 删除元素

    4.3.1 removeFirst()

    删除第一个节点,先获取首节点,判断第一个节点是不是为空,如果为空则证明没有该节点,抛出异常,内部调用的其实是unlinkFirst()。返回值是被移除的节点里面的数值。

        public E removeFirst() {
            final Node<E> f = first;
            if (f == null)
                throw new NoSuchElementException();
            return unlinkFirst(f);
        }
            // 移除首节点
        private E unlinkFirst(Node<E> f) {
            // assert f == first && f != null;
              // 获取里面的元素
            final E element = f.item;
              // 保存下一个节点
            final Node<E> next = f.next;
              // 将之前的首节点前后节点引用置空,有利于GC
            f.item = null;
            f.next = null; // help GC
              // 首节点更新
            first = next;
              // 如果首节点是空的,那么链表没有元素了,最后一个节点自然也是null
            if (next == null)
                last = null;
            else
                  // 否则当前的第一个节点的前置节点置null
                next.prev = null;
              // 链表大小-1
            size--;
              // 修改次数增加
            modCount++;
            return element;
        }

    4.3.2 removeLast()

    删除最后一个节点,和上面的删除首节点差不多,先取出最后一个节点,判断是否为空,如果为空则抛出异常,否则会调用另一个解除连接的函数unLinkLast()

        public E removeLast() {
            final Node<E> l = last;
            if (l == null)
                throw new NoSuchElementException();
            return unlinkLast(l);
        }
        private E unlinkLast(Node<E> l) {
            // assert l == last && l != null;
              // 保存被移除的节点的item
            final E element = l.item;
              // 获取上一个节点
            final Node<E> prev = l.prev;
              // 前后引用置空,有利于垃圾回收
            l.item = null;
            l.prev = null; // help GC
              // 更新最后一个节点
            last = prev;
              // 如果前置节点为空,那么链表已经没有元素了
            if (prev == null)
                first = null;
            else
                  // 否则将上一个节点的next置null
                prev.next = null;
              // 大小该表
            size--;
              // 修改次数增加
            modCount++;
              // 返回被移除的节点的item值
            return element;
        }
    

    4.3.3 remove(Object o)

    删除某个元素分为两种情况,元素为null和非null,直接遍历判断,里面真正删除的方法其实是unlink(E e),成功移除则返回true,注意这里只会移除掉第一个,后续要是还有该节点,不会移除。

        public boolean remove(Object o) {
              // 元素为null
            if (o == null) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
                  // 元素不为null
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item)) {
                          // 移除节点
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        }

    unLink(E e)方法如下:

        E unlink(Node<E> x) {
            // assert x != null;
              // 保存被移除节点的item
            final E element = x.item;
              // 下一个节点
            final Node<E> next = x.next;
              // 上一个节点
            final Node<E> prev = x.prev;
              // 如果前置节点为空,那么首节点就是当前节点了
            if (prev == null) {
                first = next;
            } else {
                  // 前一个节点的next置为下一个节点
                prev.next = next;
                  // 之前的节点的前一个节点置null
                x.prev = null;
            }
              // 如果next是空的,那么上一个节点就是现在最后一个节点
            if (next == null) {
                last = prev;
            } else {
                  // next的上一个节点引用指向prev
                next.prev = prev;
                  // 被删除的元素的next置空
                x.next = null;
            }
              // item置空
            x.item = null;
              // 大小改变
            size--;
              // 修改次数增加
            modCount++;
              // 返回被删除的节点里面的item
            return element;
        }

    4.3.4 clear()

    移除里面所有的元素:

        public void clear() {
            // Clearing all of the links between nodes is "unnecessary", but:
            // - helps a generational GC if the discarded nodes inhabit
            //   more than one generation
            // - is sure to free memory even if there is a reachable Iterator
            for (Node<E> x = first; x != null; ) {
                  // 保存下一个
                Node<E> next = x.next;
                  // 当前元素置空
                x.item = null;
                x.next = null;
                x.prev = null;
                x = next;
            }
              // 首节点和尾节点全部置null
            first = last = null;
            size = 0;
            modCount++;
        }

    4.3.5 remove(int index)

    移除指定索引的元素。先通过索引找到节点,再移除指定的节点

        public E remove(int index) {
              // 检查合法性
            checkElementIndex(index);
              // 先找到节点,再移除指定节点
            return unlink(node(index));
        }

    4.4 更新元素

    4.4.1 set(int index,E element)

    更新指定索引的位置的元素,首先通过索引查找到该元素,然后修改item值,返回旧的item值。

        public E set(int index, E element) {
              // 检查索引是否合法
            checkElementIndex(index);
              // 通过索引查找到节点
            Node<E> x = node(index);
              // 保存旧的值
            E oldVal = x.item;
              // 修改
            x.item = element;
              // 返回旧的元素
            return oldVal;
        }

    5 queue相关的方法

    因为LinkedList也实现了queue接口,所以它肯定也实现了相关的方法,下面我们看看:

    5.1 peek()

    获取队列第一个元素:

        public E peek() {
              // 拿到第一个元素,final不可变
            final Node<E> f = first;
              // 返回item值
            return (f == null) ? null : f.item;
        }

    5.2 element()

    也是获取队列第一个元素,里面调用的是getFirst()

        public E element() {
            return getFirst();
        }

    5.3 poll()

    移除队列第一个节点元素并返回,里面调用的其实是unlinkFirst()

        public E poll() {
              // 获取到第一个元素
            final Node<E> f = first;
              // 移除并返回
            return (f == null) ? null : unlinkFirst(f);
        }

    5.4 remove()

    移除队列第一个元素,里面调用的是removeFirst():

        public E remove() {
            return removeFirst();
        }

    5.5 offfer(E e)

    在队列后面增加元素:

        public boolean offer(E e) {
            return add(e);
        }

    5.6 offerFirst(E e)

    往队列的前面插入元素,其实调用的是addFirst()

        public boolean offerFirst(E e) {
            addFirst(e);
            return true;
        }

    5.7 offerLast(E e)

    往队列的后面添加元素,其实调用的是addList()

        public boolean offerLast(E e) {
            addLast(e);
            return true;
        }

    5.8 peekFirst()

    获取第一个节点里面的元素:

        public E peekFirst() {
            final Node<E> f = first;
            return (f == null) ? null : f.item;
         }

    5.9 peekLast()

    获取最后一个节点的元素:

        public E peekLast() {
            final Node<E> l = last;
            return (l == null) ? null : l.item;
        }

    5.10 pollFirst()

    获取第一个元素,并且移除它,使用的是unlinkFirst(E e)

        public E pollFirst() {
            final Node<E> f = first;
            return (f == null) ? null : unlinkFirst(f);
        }

    5.11 pollLast()

    获取队列最后一个元素,并且移除它,调用的其实是unlinkLast(E e)

        public E pollLast() {
            final Node<E> l = last;
            return (l == null) ? null : unlinkLast(l);
        }

    5.12 push(E e)

    像是堆栈的特点,在前面添加元素:

        public void push(E e) {
            addFirst(e);
        }

    5.13 pop()

    堆栈的特点,取出队列首的第一个元素

        public E pop() {
            return removeFirst();
        }

    5.14 removeFirstOccurrence(Object o)

    移除元素,从前往后第一次出现的地方移除掉:

        public boolean removeFirstOccurrence(Object o) {
            return remove(o);
        }

    5.15 removeLastOccurrence(Object o)

    移除元素,最后一次出现的地方移除掉,和前面分析的一样,分为两种情况,null和非null。

        public boolean removeLastOccurrence(Object o) {
              // 元素为null
            if (o == null) {
                for (Node<E> x = last; x != null; x = x.prev) {
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
                  // 元素不是null
                for (Node<E> x = last; x != null; x = x.prev) {
                    if (o.equals(x.item)) {
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        }

    6.其他方法

    是否包含某个元素,其实调用的是indexOf()方法,如果返回的索引不为-1,则包含:

        public boolean contains(Object o) {
            return indexOf(o) != -1;
        }

    返回大小:

        public int size() {
            return size;
        }

    是否为有效元素下标索引,从0到size-1

        private boolean isElementIndex(int index) {
            return index >= 0 && index < size;
        }

    是否为有效位置索引,从0到size

        private boolean isPositionIndex(int index) {
            return index >= 0 && index <= size;
        }

    获取指定索引位置的ListIterator:

        public ListIterator<E> listIterator(int index) {
              // 检查合法性
            checkPositionIndex(index);
            return new ListItr(index);
        }

    获取倒序的迭代器:

        public Iterator<E> descendingIterator() {
            return new DescendingIterator();
        }

    拷贝克隆函数,一个是父类的克隆函数,另一个是重写的克隆函数,这里比较特殊,因为LinkedList是链表,本身只保存了第一个和最后一个的引用,所以拷贝的时候需要向里面添加元素的方式进行拷贝。

        public Object clone() {
            LinkedList<E> clone = superClone();
    
            // Put clone into "virgin" state
            clone.first = clone.last = null;
            clone.size = 0;
            clone.modCount = 0;
    
            // 添加元素到拷贝的队列中
            for (Node<E> x = first; x != null; x = x.next)
                clone.add(x.item);
    
            return clone;
        }
        private LinkedList<E> superClone() {
            try {
                return (LinkedList<E>) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new InternalError(e);
            }
        }

    转换成为数组,通过循环实现

        public Object[] toArray() {
            Object[] result = new Object[size];
            int i = 0;
              // 循环实现
            for (Node<E> x = first; x != null; x = x.next)
                result[i++] = x.item;
            return result;
        }

    转换成为指定类型的数组,和前面不同的是,这里初始化的时候使用类型反射创建(T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size)

        public <T> T[] toArray(T[] a) {
            if (a.length < size)
                a = (T[])java.lang.reflect.Array.newInstance(
                                    a.getClass().getComponentType(), size);
            int i = 0;
            Object[] result = a;
            for (Node<E> x = first; x != null; x = x.next)
                result[i++] = x.item;
    
            if (a.length > size)
                a[size] = null;
    
            return a;
        }

    获取可分割迭代器:

        public Spliterator<E> spliterator() {
            return new LLSpliterator<E>(this, -1, 0);
        }

    7.迭代器

    里面定义了三种迭代器,都是以内部类的方式实现,分别是:

    • ListItr:列表的经典迭代器
    • DescendingIterator:倒序迭代器
    • LLSpliterator:可分割迭代器

    7.1 ListItr

    先来说说ListItr,这个迭代器主要是有next(),hashNext(),hasPrevious(),previous(),nextIndex(),previousIndex(),remove(),set(),add(),forEachRemaining()方法:

    • next():获取下一个元素
    • hashNext():是否有下一个元素
    • hasPrevious():是否有上一个元素
    • previous():上一个元素
    • nextIndex():下一个索引位置
    • previousIndex():上一个索引位置
    • remove():删除当前索引位置的元素
    • set():更新元素
    • add():新增元素
    • forEachRemaining():遍历剩下的元素

    里面主要有集合重要的属性:

    • lastReturned:上一次返回的元素
    • next:下一个返回的元素
    • nextIndex:下一个索引
    • expectedModCount:期待修改的次数
        private class ListItr implements ListIterator<E> {
              // 上一个返回的元素
            private Node<E> lastReturned;
              // 下一个元素
            private Node<E> next;
              // 下一个索引
            private int nextIndex;
              // 期待的修改次数
            private int expectedModCount = modCount;
              
              // 初始化
            ListItr(int index) {
                // 根据索引位置更新下一个返回的节点
                next = (index == size) ? null : node(index);
                  // 更新索引
                nextIndex = index;
            }
              // 是否有下一个元素:索引是否小于size
            public boolean hasNext() {
                return nextIndex < size;
            }
              // 获取下一个元素
            public E next() {
                  // 检查修改合法化
                checkForComodification();
                  // 如果没有下一个元素会抛异常,所以使用前需要先判断
                if (!hasNext())
                    throw new NoSuchElementException();
                  // 上一次返回的元素更新
                lastReturned = next;
                  // 更新下一次返回的元素
                next = next.next;
                  // 更新索引
                nextIndex++;
                  // 返回item
                return lastReturned.item;
            }
          
              // 是否有上一个:下一个返回的元素索引是不是大于0
            public boolean hasPrevious() {
                return nextIndex > 0;
            }
              // 返回上一个元素
            public E previous() {
                  // 检查
                checkForComodification();
                  // 判断是否有上一个元素  
                  if (!hasPrevious())
                    throw new NoSuchElementException();
                  // 上一个返回的元素,需要更新
                lastReturned = next = (next == null) ? last : next.prev;
                // 更新索引
                  nextIndex--;
                return lastReturned.item;
            }
              // 下一个索引
            public int nextIndex() {
                return nextIndex;
            }
    
              // 上一个索引
            public int previousIndex() {
                return nextIndex - 1;
            }
        
              // 移除当前位置的索引
            public void remove() {
                  // 检查修改合法性
                checkForComodification();
                if (lastReturned == null)
                    throw new IllegalStateException();
                            // 获取下一个元素
                Node<E> lastNext = lastReturned.next;
                  // 移除上一个返回的元素
                unlink(lastReturned);
                  // 如果下一个是上次返回的元素,那么下一个元素需要更新,因为该元素已经被移除了
                if (next == lastReturned)
                    next = lastNext;
                else
                      // 更新索引
                    nextIndex--;
                lastReturned = null;
                expectedModCount++;
            }
    
              // 更新
            public void set(E e) {
                if (lastReturned == null)
                    throw new IllegalStateException();
                checkForComodification();
                lastReturned.item = e;
            }
    
            public void add(E e) {
                checkForComodification();
                lastReturned = null;
                  // 如果下一个元素是空,那就是在队尾添加元素
                if (next == null)
                    linkLast(e);
                else
                      // 否则就是在next索引处添加元素
                    linkBefore(e, next);
                  // 更新索引
                nextIndex++;
                expectedModCount++;
            }
                    
              // 遍历剩下的元素
            public void forEachRemaining(Consumer<? super E> action) {
                Objects.requireNonNull(action);
                  // 使用循环,索引不断后移,遍历
                while (modCount == expectedModCount && nextIndex < size) {
                      // 对每个节点元素执行操作
                    action.accept(next.item);
                    lastReturned = next;
                    next = next.next;
                    nextIndex++;
                }
                checkForComodification();
            }
    
            final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
        }

    上面的迭代器没有什么好说的,就是往前面和后面遍历的功能,以及增删改的功能。

    7.2 DescendingIterator

    这个迭代器有点意思,也很简单,就是一个倒序的功能,功能实现也十分简单:

    • hasNext:是否有下一个元素,实际上是判断上一个元素
    • next:获取下一个元素,实际上是获取前面一个元素
    • remove:移除元素

    倒序就是别人从前往后,它偏偏从后往前遍历,emmmmmmm

        private class DescendingIterator implements Iterator<E> {
            private final ListItr itr = new ListItr(size());
            public boolean hasNext() {
                return itr.hasPrevious();
            }
            public E next() {
                return itr.previous();
            }
            public void remove() {
                itr.remove();
            }
        }

    7.3 LLSpliterator

    这个迭代器有点东西,感觉和其它的不太一样,LLSpliterator是在使用node的next进行迭代,下面分析一下:主要是为了将元素分为多份,然后再用多线程来处理。

    值得注意的是:分割的时候,LinkedList不是1/2分割,而是每一次分割出来的大小都是递增的,递增的大小是BATCH_UNIT,但是返回的不是LLSpliterator,而是ArraySpliterator,每次都分割出更多的元素,转成数组结构,这也许是出自于性能考虑,比较指针遍历太慢了,我猜的的...别打我

        static final class LLSpliterator<E> implements Spliterator<E> {
              // 分割长度增加单位
            static final int BATCH_UNIT = 1 << 10;  // batch array size increment
              // 最大分割长度
            static final int MAX_BATCH = 1 << 25;  // max batch array size;
            final LinkedList<E> list; // null OK unless traversed
              // 当前节点
            Node<E> current;      // current node; null until initialized
              // 大小估算
            int est;  
              // 期待修改的次数
            int expectedModCount; // initialized when est set
              // 分割长度
            int batch;            // batch size for splits
    
            LLSpliterator(LinkedList<E> list, int est, int expectedModCount) {
                this.list = list;
                this.est = est;
                this.expectedModCount = expectedModCount;
            }
    
            final int getEst() {
                int s; // force initialization
                final LinkedList<E> lst;
                if ((s = est) < 0) {
                    if ((lst = list) == null)
                        s = est = 0;
                    else {
                        expectedModCount = lst.modCount;
                        current = lst.first;
                        s = est = lst.size;
                    }
                }
                return s;
            }
                    // 估算大小
            public long estimateSize() { return (long) getEst(); }
    
              // 分割
            public Spliterator<E> trySplit() {
                Node<E> p;
                  // 获取大小
                int s = getEst();
                  // 当前节点不为空
                if (s > 1 && (p = current) != null) {
                      // 分割位置结束:分割位置+分割单位
                    int n = batch + BATCH_UNIT;
                      // 如果大于大小,就限制最后的位置
                    if (n > s)
                        n = s;
                      // 最大的分割位置
                    if (n > MAX_BATCH)
                        n = MAX_BATCH;
                      // 数组
                    Object[] a = new Object[n];
                    int j = 0;
                      // 将当前位置到n的位置循环,存放到a数组中
                    do { a[j++] = p.item; } while ((p = p.next) != null && j < n);
                    current = p;
                    batch = j;
                    est = s - j;
                      // ArraySpliterator每次分割成一半一半,而IteratorSpliterator算术递增
                    return Spliterators.spliterator(a, 0, j, Spliterator.ORDERED);
                }
                return null;
            }
    
              // 对剩下的元素进行处理
            public void forEachRemaining(Consumer<? super E> action) {
                Node<E> p; int n;
                if (action == null) throw new NullPointerException();
                if ((n = getEst()) > 0 && (p = current) != null) {
                    current = null;
                    est = 0;
                    do {
                        E e = p.item;
                        p = p.next;
                        action.accept(e);
                    } while (p != null && --n > 0);
                }
                if (list.modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
    
              // 对后面一个元素进行处理
            public boolean tryAdvance(Consumer<? super E> action) {
                Node<E> p;
                if (action == null) throw new NullPointerException();
                if (getEst() > 0 && (p = current) != null) {
                    --est;
                    E e = p.item;
                    current = p.next;
                    action.accept(e);
                    if (list.modCount != expectedModCount)
                        throw new ConcurrentModificationException();
                    return true;
                }
                return false;
            }
    
            public int characteristics() {
                return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
            }
        }

    8.序列化和反序列化

    序列化和反序列化的时候,需要重写,因为我们保存的只有第一个和最后一个节点的引用,我们序列化需要保存大小和引用,所以需要重写,要不反序列化回来就找不到next,节点之间的关系就会丢失。

    序列化的时候如下,写入了size,以及遍历的时候将节点的item值写入。

        private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException {
            // Write out any hidden serialization magic
            s.defaultWriteObject();
    
            // Write out size
            s.writeInt(size);
    
            // Write out all elements in the proper order.
            for (Node<E> x = first; x != null; x = x.next)
                s.writeObject(x.item);
        }

    反序列化的时候,读入大小size以及每个节点里面的元素item

        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            // 默认序列化
            s.defaultReadObject();
    
            // 大小
            int size = s.readInt();
    
            // 按照顺序读入元素
            for (int i = 0; i < size; i++)
                linkLast((E)s.readObject());
        }

    9.总结一下

    • LinkedList底层是用链表实现的,而且是双向链表,并且实现了Queue接口,可以当成双向队列或者堆栈来使用。也正是因为是链表实现,所以删除元素比较快,但是查找的时候相对较慢。当然,也没有什么扩容,除非就是内存不够了。
    • 双向链表,可以从头往尾遍历,也可以从尾部往前遍历。
    • LinkedList继承了AbstractSequentialListAbstractSequentialList实现了get,set,add,remove等方法。
    • 序列化/反序列化的时候重写了方法,才能达到序列化里面每一个节点元素的效果。
    • 线程不安全

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    查看原文

    赞 1 收藏 0 评论 0

    秦怀杂货店 发布了文章 · 2020-11-28

    【Java基础】-- isAssignableFrom的用法详细解析

    [TOC]
    最近在java的源代码中总是可以看到isAssignableFrom()这个方法,到底是干嘛的?怎么用?

    1. isAssignableFrom()是干什么用的?

    首先我们必须知道的是,java里面一切皆对象,类本身也是会当成对象来处理,主要体现在类的.class文件,其实加载到java虚拟机之后,也是一个对象,它就是Class对象,全限定类名:java.lang.Class

    那这个isAssignableFrom()其实就是Class对象的一个方法:

        /**
         * Determines if the class or interface represented by this
         * {@code Class} object is either the same as, or is a superclass or
         * superinterface of, the class or interface represented by the specified
         * {@code Class} parameter. It returns {@code true} if so;
         * otherwise it returns {@code false}. If this {@code Class}
         * object represents a primitive type, this method returns
         * {@code true} if the specified {@code Class} parameter is
         * exactly this {@code Class} object; otherwise it returns
         * {@code false}.
         *
         * <p> Specifically, this method tests whether the type represented by the
         * specified {@code Class} parameter can be converted to the type
         * represented by this {@code Class} object via an identity conversion
         * or via a widening reference conversion. See <em>The Java Language
         * Specification</em>, sections 5.1.1 and 5.1.4 , for details.
         *
         * @param cls the {@code Class} object to be checked
         * @return the {@code boolean} value indicating whether objects of the
         * type {@code cls} can be assigned to objects of this class
         * @exception NullPointerException if the specified Class parameter is
         *            null.
         * @since JDK1.1
         */
        public native boolean isAssignableFrom(Class<?> cls);

    native关键字描述,说明是一个底层方法,实际上是使用c/c++实现的,java里面没有实现,那么这个方法是干什么的呢?我们从上面的注释可以解读:

    如果是A.isAssignableFrom(B)
    确定一个类(B)是不是继承来自于另一个父类(A),一个接口(A)是不是实现了另外一个接口(B),或者两个类相同。主要,这里比较的维度不是实例对象,而是类本身,因为这个方法本身就是Class类的方法,判断的肯定是和类信息相关的。

    也就是判断当前的Class对象所表示的类,是不是参数中传递的Class对象所表示的类的父类,超接口,或者是相同的类型。是则返回true,否则返回false。

    2.代码实验测试

    2.1 父子继承关系测试

    class A{
    }
    class B extends A{
    }
    class C extends B{
    }
    public class test {
        public static void main(String[] args) {
            A a = new A();
            B b = new B();
            B b1 = new B();
            C c = new C();
            System.out.println(a.getClass().isAssignableFrom(a.getClass()));
            System.out.println(a.getClass().isAssignableFrom(b.getClass()));
            System.out.println(a.getClass().isAssignableFrom(c.getClass()));
            System.out.println(b1.getClass().isAssignableFrom(b.getClass()));
    
            System.out.println(b.getClass().isAssignableFrom(c.getClass()));
    
            System.out.println("=====================================");
            System.out.println(A.class.isAssignableFrom(a.getClass()));
            System.out.println(A.class.isAssignableFrom(b.getClass()));
            System.out.println(A.class.isAssignableFrom(c.getClass()));
    
            System.out.println("=====================================");
            System.out.println(Object.class.isAssignableFrom(a.getClass()));
            System.out.println(Object.class.isAssignableFrom(String.class));
            System.out.println(String.class.isAssignableFrom(Object.class));
        }
    }

    运行结果如下:

    true
    true
    true
    true
    true
    =====================================
    true
    true
    true
    =====================================
    true
    true
    false

    从运行结果来看,C继承于BB继承于A,那么任何一个类都可以isAssignableFrom其本身,这个从中文意思来理解就是可以从哪一个装换而来,自身装换而来肯定是没有问题的,父类可以由子类装换而来也是没有问题的,所以A可以由B装换而来,同时也可以由子类的子类转换而来。

    上面的代码也说明一点,所有的类,其最顶级的父类也是Object,也就是所有的类型都可以转换成为Object

    2.2 接口的实现关系测试

    interface InterfaceA{
    }
    
    class ClassB implements InterfaceA{
    
    }
    class ClassC implements InterfaceA{
    
    }
    class ClassD extends ClassB{
    
    }
    public class InterfaceTest {
        public static void main(String[] args) {
            System.out.println(InterfaceA.class.isAssignableFrom(InterfaceA.class));
            System.out.println(InterfaceA.class.isAssignableFrom(ClassB.class));
            System.out.println(InterfaceA.class.isAssignableFrom(ClassC.class));
            System.out.println(ClassB.class.isAssignableFrom(ClassC.class));
            System.out.println("============================================");
    
            System.out.println(ClassB.class.isAssignableFrom(ClassD.class));
            System.out.println(InterfaceA.class.isAssignableFrom(ClassD.class));
        }
    }

    输出结果如下:

    true
    true
    true
    false
    ============================================
    true
    true

    从上面的结果看,其实接口的实现关系和类的实现关系是一样的,没有什么区别,但是如果BC都实现了同一个接口,他们之间其实是不能互转的。

    如果B实现了接口AD继承了B,实际上D是可以上转为A接口的,相当于D间接实现了A,这里也说明了一点,其实继承关系和接口实现关系,在isAssignableFrom()的时候是一样的,一视同仁的。

    3.总结

    isAssignableFrom是用来判断子类和父类的关系的,或者接口的实现类和接口的关系的,默认所有的类的终极父类都是Object。如果A.isAssignableFrom(B)结果是true,证明B可以转换成为A,也就是A可以由B转换而来。

    这个方法在我们平时使用的不多,但是很多源码里面判断两个类之间的关系的时候,(注意:是两个类的关系,不是两个类的实例对象的关系!!!),会使用到这个方法来判断,大概因为框架代码或者底层代码都是经过多层抽象,做到容易拓展和解耦合,只能如此。

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

    此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~

    查看原文

    赞 0 收藏 0 评论 0