Java 工程师成神之路-基础总结


jdk8新特性方面

  • 接口的默认和静态方法
    Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法。

  • 方法引用
    方法引用通过方法的名字来指向一个方法。

public static void main(String args[]){
      List<String> names = new ArrayList();
      names.add("Google");
      names.add("Tencent");
      names.forEach(System.out::println);
   }
  1. 构造器引用:它的语法是Class::new,或者更一般的Class< T >::new实例如下:
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
  1. 静态方法引用:它的语法是Class::static_method,实例如下:
cars.forEach( Car::collide );
  1. 特定类的任意对象的方法引用:它的语法是Class::method实例如下:
cars.forEach( Car::repair );
  1. 特定对象的方法引用:它的语法是instance::method实例如下:
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
  • Lambda 表达式 与 Stream的使用
 List<Integer> r = l.stream()
                .map(e -> new Integer(e))
                .filter(e -> Primes.isPrime(e))
                .distinct()
                .collect(Collectors.toList());
  • Optional
    Optional类实际上是个容器:它可以保存类型T的值,或者保存null。使用Optional类我们就不用显式进行空指针检查了。
  • Date Time API − 加强对日期与时间的处理。
    线程安全的LocalDateTime。
  • CurrentHashMap做了升级。

Java基本类型

计算机的基本单位:bit . 一个bit代表一个0或1

byte(最小的数据类型-字节):1byte = 8bit 1个字节是8个bit
short:2byte
int:4byte
long:8byte
float(浮点型):4byte
double(双精度浮点型):8byte
boolean:1byte
char:2byte

为什么不能用浮点数来表示金额

精度丢失:float的精度是23位,double精度是63位。在存储或运算过程中,当超出精度时,超出部分会被截掉,由此就会造成误差。
进制转换误差:A进制下的有限小数,转换到B进制下极有可能是无限小数,误差也由此产生。

包装类型的缓存

  1. 具有缓存机制的类
    Byte、Short、Integer、Long、Character都具有缓存机制。缓存工作都是在静态块中完成,在类生命周期初始化阶段执行。
  2. 缓存范围
    Byte,Short,Integer,Long为 -128 到 127;Character范围为 0 到 127

字节字符区别

字节是存储容量的基本单位,字符是数子,字母,汉子以及其他语言的各种符号。
1 字节=8 个二进制单位:一个一个字符由一个字节或多个字节的二进制单位组成。

short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 +=1;有什么错?

  1. 在 s1+1 运算时会自动提升表达式的类型为 int, 那么将 int 赋予给 short 类型的变量 s1 会出现类型转换错误
  2. +=是 java 语言规定的运算符,java 编译器会对它 进行特殊处理,因此可以正确编译。

关键字transient

  • 被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问
  • transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
  • 被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。(JVM中的)

泛型

泛型,即“参数化类型”。
将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),
然后在使用/调用时传入具体的类型(类型实参)。

泛型只在编译阶段有效

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
    Log.d("泛型测试","类型相同");
}

Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

泛型的使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;
    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }
    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}
  • 泛型的类型参数只能是类类型,不能是简单类型。
  • 不能对确切的泛型类型使用instanceof操作。

泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}

Object obj = genericMethod(Class.forName("com.test.test"));

泛型上下边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

public void showKeyValue1(Generic<? extends Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

值传递与引用传递

参数传递基本上就是赋值操作。

基本类型 和 引用类型的不同之处

num是基本类型,值就直接保存在变量中。
而str是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。

int num = 10;
String str = "hello";

赋值运算符(=)的作用

对于基本类型 num ,赋值运算符会直接改变变量的值,原来的值被覆盖掉。
对于引用类型 str,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变(重要)。

num = 20;
str = "java";

调用方法时发生了什么

第二个例子:没有提供改变自身方法的引用类型
void foo(String text) {
    text = "windows";
}
foo(str); // str 也没有被改变

第三个例子:提供了改变自身方法的引用类型
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
    builder.append("4");
}
foo(sb); // sb 被改变了,变成了"iphone4"。

第四个例子:提供了改变自身方法的引用类型,但是不使用,而是使用赋值运算符。
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
    builder = new StringBuilder("ipad");
}
foo(sb); // sb 没有被改变,还是 "iphone"。
  • 值传递:传递的是实际值,像基本数据类型
  • 引用传递:将对象的引用作为实参进行传递
    Java中其实还是值传递的,只不过对于对象参数,值的内容是对象的引用。
public static void main(String[] args) {
        // 自动装箱就是jdk调用了Integer的valueOf(int)的方法
        Integer a = 1;
        Integer b = 2;
        System.out.printf("a = %s, b = %s\n", a, b);
        // 这里传入的是实参 java基本类型数据作为参数是值传递,对象类型是引用传递
        swap(a, b);
        System.out.printf("a = %s, b = %s\n", a, b);
    }

    /**
     * 这里接受的是形参
     * 从作用域上看,形参只会在方法内部生效,方法结束后,形参也会被释放掉,所以形参是不会影响方法外的
     * @param a
     * @param b
     */
    public static void swap(Integer a, Integer b) {
        // TODO 错误实现
//        Integer temp = a;
//        a = b;
//        b = temp;
        int temp = a.intValue();
        try {
            // 要想交换值的话,需要借助反射
            Field value = Integer.class.getDeclaredField("value");
            value.setAccessible(true);
            value.set(a, b);
            value.set(b, new Integer(temp));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

数据深浅拷贝

ArrayList.addAll()、ArrayLis.clone() 两个方法都是对数据的浅拷贝,操作的还是同一份内存。
可以implements Cloneable,重写clone方法 (或者序列化后再反序列化回来)。

for(int i=0;i
copy.add((A)src.get(i).clone());
}

就可以完成深度拷贝了。

Java中的浅度复制是不会把要复制的那个对象的引用对象重新开辟一个新的引用空间,当我们需要深度复制的时候,这个时候我们就要重写clone()方法。

集合类

Collection和Collections的区别

  1. java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。
 Collection   
├List   
│├LinkedList   
│├ArrayList   
│└Vector   
│ └Stack   
└Set
  1. Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

ConcurrentSkipListMap

ConcurrentSkipListMap其内部采用SkipList数据结构实现。
为了实现SkipList,ConcurrentSkipListMap提供了三个内部类来构建这样的链表结构:Node、Index、HeadIndex。其中Node表示最底层的单链表有序节点、Index表示为基于Node的索引层,HeadIndex用来维护索引层次。
ConcurrentSkipListMap是通过HeadIndex维护索引层次,通过Index从最上层开始往下层查找,一步一步缩小查询范围,最后到达最底层Node时,就只需要比较很小一部分数据了。

PriorityQueue

PriorityQueue 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。

  1. PriorityQueue是一种无界的,线程不安全的队列
  2. PriorityQueue是一种通过数组实现的,并拥有优先级的队列
  3. PriorityQueue存储的元素要求必须是可比较的对象, 如果不是就必须明确指定比较器

序列化

序列化与反序列化

当两个Java进程进行通信时,实现进程间的对象传送。序列化是一种用来处理对象流的机制 ,所谓对象流就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。ObjectOutputStream:表示对象输出流;ObjectInputStream:表示对象输入流;

声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。
序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联
如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因

为什么序列化可以破坏单例

序列化会通过反射调用无参数的构造方法创建一个新的对象。
在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

怎么防御反序列化攻击(完整性校验:如JWT)

只要是从Application之外读取或接收数据,并将其反序列化成Application或API中的对象,都可能存在反序列化安全问题。

枚举

枚举比较

枚举是作为抽象类存在的,每个枚举类型都是作为一个static final字段存在,并且每个枚举类型在内部是作为子类实现的,并且枚举对象是单例的。
对于枚举的比较直接比较内存地址就可以了

public final boolean equals(Object var1) {
        return this == var1;// Enum的源码,可以发现Enum重写了equals且禁止重写,内部也是用的==实现的
    }

枚举序列化

为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的。在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。

以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象

字符流、字节流

字符流(Writer/Reader)

  1. 字符流处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串。
  2. 只能处理字符或者字符串。(不可以拷贝非纯文本的文件)
  3. 在操作的时候是使用到缓冲区的。如果字符流不调用close或flush方法,则不会输出任何内容。

字节流(OutputStream/InputStream)

  1. 字节流处理单元为1个字节,操作字节和字节数组。
  2. 可用于任何类型
  3. 在操作的时候本身是不会用到缓冲区的,是与文件本身直接操作的,所以字节流在操作文件时,即使不关闭资源,文件也能输出。

read()方法读取的是一个字节但返回是int:列如图片音频等,这些文件底层都是以二进制形式的存储的,如果每次读取都返回byte,有可能在读到中间的时候遇到111111111;那么这11111111是byte类型的-1,我们的程序是遇到-1就会停止不读了,后面的数据就读不到了,所以在读取的时候用int类型接收,如果11111111会在其前面补上

JMS、JMX

JMS:Java消息服务是一个与具体平台无关的API。(点对点通信模型、发布/订阅通信模型)

  • JMS 原本就是一个异步的消息服务,客户端获取消息的时候,不需要主动发送请求,消息会自动发送给可用的客户
  • JMS保证消息只会递送一次。大家都遇到过重复创建消息问题,而JMS能帮你避免该问题。

JMX最常见的场景是监控Java程序的基本信息和运行情况,任何Java程序都可以开启JMX,然后使用JConsole或Visual VM进行预览。

泛型

泛型与继承

泛型是参数化类型,把运行时期可能产生的问题,提前到了编译时期,用来保证代码安全性。父类为泛型,子类继承时:范围大于或等于。

类型擦除

Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

强弱软虚引用

强引用

强引用就是程序中一般使用的引用类型,强引用的对象是可触及的,不会被回收。相对的,软引用、弱引用和虚引用的对象是软可触及的、弱可触及的和虚可触及的,在一定条件下,都是可以被回收的。

StringBuffer str = new StringBuffer("Hello world");

假设以上代码是在函数体内运行的,那么局部变量str将被分配在栈上,而对象StringBuffer实例被分配在堆上。局部变量str指向StringBuffer实例所在堆空间,通过str可以操作该实例,那么str就是StringBuffer实例的强引用。

  • 强引用可以直接访问目标对象
  • 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指的对象。
  • 强引用可能导致内存泄漏

软引用----可被回收的引用

软引用是比强引用弱一点的引用类型,一个对象只持有软引用,那么当堆空间不足时,就会被回收。软引用使用java.lang.ref.SoftReference类型。

  • GC未必会回收软引用的对象,但是当内存资源紧张时,软引用对象会被回收,所以软引用对象不会引起内存溢出。

弱引用-----GC发现即回收

弱引用是一种比软引用较弱的引用类型。在系统GC时,只要发现弱引用,不管系统堆空间使用情况如何,都会将对象进行回收。但是由于垃圾回收器的优先级通常很低,因此,并不一定能很快的发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间,一旦一个弱引用对象被垃圾回收器回收,便会加入一个注册的引用队列(这一点和软引用很像)。弱引用使用java.lang.ref.WeakReference类实现。

  • 软引用、弱引用都非常适合来保存那些可有可无的缓存数据(应用场景)。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存的溢出,而当内存资源充足时,这些缓存又可以存在相当长的时间,从而起到加速系统的作用。

虚引用----对象回收跟踪

一个持有虚引用的对象,和没有引用几乎一样。随时都可能被垃圾回收器回收,当试图通过虚引用的get()方法取得强引用时,总是会失败。
并且,虚引用必须和引用队列一起使用,它的作用主要是跟踪垃圾回收过程
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知引用程序对象的回收情况。


版权声明:本文为wantLight原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。