java 基础(一)

java语法

抽象类和接口的区别

抽象类和接口的对比

java 基础(一)_第1张图片

 什么时候使用抽象类或接口

  • 如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类吧。
  • 如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
  • 如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。

java8中的默认方法和静态方法

为了解决Java接口中只能定义抽象方法的问题。Java8新增加了默认方法的特性。

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();                          
     static ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }
    //默认方法 
    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}

当我们继承含有默认方法的接口时,一般有以下三种情况:

  • 不去管默认方法,继承的接口直接继承默认方法
  • 重新声明默认方法,这样会使得这个方法变成抽象方法
  • 重新定义默认方法,这样会使得方法被重写

在Java8的接口中,我们不光能写默认方法,还能写静态方法。上面的例子中正好用到了静态方法。

封装、继承

封装的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。

继承是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果。

overide overload(多态)

重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的。

Override(重写)

  • 方法名、参数、返回值相同。
  • 子类方法不能缩小父类方法的访问权限。
  • 子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。例如: 父类的一个方法申明了一个检查异常IOException,在重写这个方法是就不能抛出Exception,只能抛出IOException的子类异常,可以抛出非检查异常。
  • 存在于父类和子类之间。
  • 方法被定义为final不能被重写。

Overload(重载)

  • 参数类型、个数、顺序至少有一个不相同。
  • 不能重载只有返回值不同的方法名。
  • 存在于父类和子类、同类中。
public int doSomething() {
return 0;
}
// 输入参数不同,意味着方法签名不同,重载的体现
public int doSomething(Lis srs) {
return 0;
}
// return类型不一样,编译不能通过
public short doSomething() {
return 0;
}

ERROR和Exception,CheckedException,RuntimeException的区别。

java 基础(一)_第2张图片

Exception

Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。

Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。

Exception又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。

受检异常

Checked Exception不兼容functional编程,如果你写过Lambda/Stream代码,相信深有体会。解决方案最简单的方法是转成非受检异常。参考:https://www.jianshu.com/p/a3855f880ff2

运行时异常

不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。

NoClassDefFoundError和ClassNotFoundException有什么区别?

一般情况下,当我们使用Class.forName()或者ClassLoader.loadClass以及使用ClassLoader.findSystemClass()在运行时加载类的时候,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException。

当JVM在加载一个类的时候,如果这个类在编译时是可用的,但是在运行时找不到这个类的定义的时候,JVM就会抛出一个NoClassDefFoundError错误。

Error

Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序(比如JVM自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常
见的比如OutOfMemoryError之类,都是Error的子类。

常见Error:

堆内存溢出 OutOfMemoryError(OOM)

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。

Java Heap 溢出。 一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。 java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。

堆栈溢出 StackOverflow (SOF)

StackOverflowError 的定义: 当应用程序递归太深而发生堆栈溢出时,抛出该错误。 因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。

栈溢出的原因:

递归调用。 大量循环或死循环。 全局变量是否过多。 数组、List、map数据过大。

throw 和 throws

throws主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获这个异常。
throw是具体向外抛异常的动作,所以它是抛出一个异常实例。

final finally finalize

final

final可以用来修饰类、方法、变量,分别有不同的意义,final修饰的class代表不可以继承扩展,final的变量是不可以修改的,而final的方法也是不可以重写的(override)。

需要注意的是,fnal并不等同于immutable。注意下面的代码:

final Lis srLis = new ArrayLis<>();
srLis.add("Hello");
srLis.add("world");
Lis unmodifableStrLis = Lis.of("hello", "world");
unmodifableStrLis.add("again");

final只能约束strList这个引用不可以被赋值,但是strList对象行为不被fnal影响,添加元素等操作是完全正常的。如果我们真的希望对象本身是不可变的,那么需要相应的类支持不可变的行为。在上面这个例子中,List.of方法创建的本身就是不可变List,最后那句add是会在运行时抛出异常的。 

final和static的区别

static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变。参考代码:

public class Test {
    public static void main(String[] args)  {
        MyClass myClass1 = new MyClass();
        MyClass myClass2 = new MyClass();
        System.out.println(myClass1.i);
        System.out.println(myClass1.j);
        System.out.println(myClass2.i);
        System.out.println(myClass2.j);
 
    }
}
class MyClass {
    public final double i = Math.random();
    public static double j = Math.random();
}
//运行结果,两次打印,j的值都是一样的,j是static类型的属于类,因此两次值相同。i不是static的因此属于对象,但是i的值是不可变的。

接口中的变量都是public static final 的。

编译器阶段

使用final关键字,如果编译器能够在编译阶段确定某变量的值,那么编译器就会把该变量当做编译期常量来使用。如果需要在运行时确定,那么编译器就不会优化相关代码。

public class Test {
    public static void main(String[] args)  {
        String a = "hello2";  
        final String b = "hello";
        String d = "hello";
        String c = b + 2;  
        String e = d + 2;
        System.out.println((a == c));
        System.out.println((a == e));
    }
}
//final类型,在编译阶段能够确定值。
//非final类型在编译阶段确定不了
输出:
true
false
public class Test {
    public static void main(String[] args)  {
        String a = "hello2";  
        final String b = getHello();
        String c = b + 2;  
        System.out.println((a == c));
 
    }
     
    public static String getHello() {
        return "hello";
    }
}
//即便是final类型,编译阶段也确定不了值。
输出
false

 

finally

肯定会执行。finally{}块的代码。 只有在try{}块中包含遇到System.exit(0)。 之类的导致Java虚拟机直接退出的语句才会不执行。

当程序执行try{}遇到return时,程序会先执行return语句,但并不会立即返回——也就是把return语句要做的一切事情都准备好,也就是在将要返回、但并未返回的时候,程序把执行流程转去执行finally块,当finally块执行完成后就直接返回刚才return语句已经准备好的结果。

try {
// do something
Sysem.exit(1);
} fnally{
Sysem.out.println(“Print from fnally”);
}

System.exit(0);将停止当前线程和所有其他当场死亡的线程。finally块并不能让已经停止的线程继续执行。 

try-with-resources

finalize

finalize是基础类java.lang.Object的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize机制现在已经不推荐使用,并且在JDK 9开始被标记为deprecated。

一旦实现了非空的finalize方法,就会导致相应对象回收呈现数量级上的变慢。因为finalize拖慢垃圾收集,导致大量对象堆积,也是一种典型的导致OOM的原因。

有什么机制可以替换finalize吗?

有什么机制可以替换fnalize吗?
Java平台目前在逐步使用java.lang.ref.Cleaner来替换掉原有的fnalize实现。Cleaner的实现利用了幻象引用(PhantomReference),这是一种常见的所谓post-mortem清理机制。利用幻象引用和引用队列,我们可以保证对象被彻底销毁前做一些类似资源回收的工作,比如关闭文件描述符(操作系统有限的资源),它比fnalize更加轻量、更加可靠。参考代码:

public class CleaningExample implements AutoCloseable {
// A cleaner, preferably one shared within a library
private satic fnal Cleaner cleaner = ;
satic class State implements Runnable {
State(...) {
// initialize State needed for cleaning action
}
public void run() {
// cleanup action accessing State, executed at mos once
}
}
private fnal State;
private fnal Cleaner.Cleanable cleanable
public CleaningExample() {
this.sate = new State(...);
this.cleanable = cleaner.regiser(this, sate);
}
public void close() {
cleanable.clean();
}
}


Object

常用方法

Object是所有类的父类,任何类都默认继承Object。

  • clone 保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
  • equals 在Object中与==是一样的,子类一般需要重写该方法。
  • hashCode 该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
  • getClass final方法,获得运行时类型
  • wait 使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。 wait() 方法一直等待,直到获得锁或者被中断。 wait(long timeout) 设定一个超时间隔,如果在规定时间内没有获得锁就返回。调用该方法后当前线程进入睡眠状态,直到以下事件发生:1、其他线程调用了该对象的notify方法。 2、其他线程调用了该对象的notifyAll方法。 3、其他线程调用了interrupt中断该线程。 4、时间间隔到了。 5、此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
  • notify 唤醒在该对象上等待的某个线程。
  • notifyAll 唤醒在该对象上等待的所有线程。
  • toString 转换成字符串,一般子类都有重写,否则打印句柄。

HashCode和equals

HashCode特性:

  • HashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,HashCode经常用于确定对象的存储地址;
  • 如果两个对象相同, equals方法一定返回true,并且这两个对象的HashCode一定相同;
  • 两个对象的HashCode相同,并不一定表示两个对象就相同,即equals()不一定为true,只能够说明这两个对象在一个散列存储结构中。
  • 如果对象的equals方法被重写,那么对象的HashCode也尽量重写。

HashCode作用:

从Object角度看,JVM每new一个Object,它都会将这个Object丢到一个Hash表中去,这样的话,下次做Object的比较或者取这个对象的时候(读取过程),它会根据对象的HashCode再从Hash表中取这个对象。这样做的目的是提高取对象的效率。若HashCode相同再去调用equal。
请看下面的例子:

public class HashTest {  
    private int i;  
  
    public int getI() {  
        return i;  
    }  
  
    public void setI(int i) {  
        this.i = i;  
    }  
  
    public int hashCode() {  
        return i % 10;  
    }  
  
    public final static void main(String[] args) {  
        HashTest a = new HashTest();  
        HashTest b = new HashTest();  
        a.setI(1);  
        b.setI(1);  
        Set set = new HashSet();  
        set.add(a);  
        set.add(b);  
        System.out.println(a.hashCode() == b.hashCode());  
        System.out.println(a.equals(b));  
        System.out.println(set);  
    }  
}  

输出结果为:

true
false
[HashTest@1, HashTest@1]

以上这个示例,我们只是重写了HashCode方法,从上面的结果可以看出,虽然两个对象的HashCode相等,但是实际上两个对象并不是相等,因为我们没有重写equals方法,那么就会调用Object默认的equals方法,显示这是两个不同的对象。 

 这里我们将生成的对象放到了HashSet中,而HashSet中只能够存放唯一的对象,也就是相同的(适用于equals方法)的对象只会存放一个,但是这里实际上是两个对象ab都被放到了HashSet中,这样HashSet就失去了他本身的意义了。

这里我们将生成的对象放到了HashSet中,而HashSet中只能够存放唯一的对象,也就是相同的(适用于equals方法)的对象只会存放一个,但是这里实际上是两个对象ab都被放到了HashSet中,这样HashSet就失去了他本身的意义了。

重写equals方法后:

public class HashTest {  
    private int i;  
  
    public int getI() {  
        return i;  
    }  
  
    public void setI(int i) {  
        this.i = i;  
    }  
  
    public boolean equals(Object object) {  
        if (object == null) {  
            return false;  
        }  
        if (object == this) {  
            return true;  
        }  
        if (!(object instanceof HashTest)) {  
            return false;  
        }  
        HashTest other = (HashTest) object;  
        if (other.getI() == this.getI()) {  
            return true;  
        }  
        return false;  
    }
  
    public int hashCode() {  
        return i % 10;  
    }  
  
    public final static void main(String[] args) {  
        HashTest a = new HashTest();  
        HashTest b = new HashTest();  
        a.setI(1);  
        b.setI(1);  
        Set set = new HashSet();  
        set.add(a);  
        set.add(b);  
        System.out.println(a.hashCode() == b.hashCode());  
        System.out.println(a.equals(b));  
        System.out.println(set);  
    }  
}  

true
true
[HashTest@1]

从结果我们可以看出,现在两个对象就完全相等了,HashSet中也只存放了一份对象。

为什么equals()相等,hashCode就一定要相等,而hashCode相等,却不要求equals相等?

1、因为是按照hashCode来访问小内存块,所以hashCode必须相等。 2、HashMap获取一个对象是比较key的hashCode相等和equal为true。

之所以hashCode相等,却可以equal不等,就比如ObjectA和ObjectB他们都有属性name,那么hashCode都以name计算,所以hashCode一样,但是两个对象属于不同类型,所以equal为false。

为什么需要hashCode?

1、通过hashCode可以很快的查到小内存块。 2、通过hashCode比较比equal方法快,当get时先比较hashCode,如果hashCode不同,直接返回false。

equals与==的区别

  • ==是判断两个变量或实例是不是指向同一个内存空间。 equals是判断两个变量或实例所指向的内存空间的值是不是相同。
  • ==是指对内存地址进行比较。 equals()是对字符串的内容进行比较。
  • ==指引用是否相同。 equals()指的是值是否相同。
ublic static void main(String[] args) {
        String a = new String("ab"); // a 为一个引用
        String b = new String("ab"); // b为另一个引用,对象的内容一样
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 从常量池中查找
        System.out.println(aa == bb); // true
        System.out.println(a == b); // false,非同一对象
        System.out.println(a.equals(b)); // true
        System.out.println(42 == 42.0);  // true
    }
public static void main(String[] args) {
    Object obj1 = new Object();
    Object obj2 = new Object();
    System.out.println(obj1.equals(obj2));//false
    System.out.println(obj1==obj2);//false
    obj1=obj2;
    System.out.println(obj1==obj2);//true
    System.out.println(obj2==obj1);//true
}

深拷贝和浅拷贝区别

将一个对象的引用复制给另外一个对象,一共有三种方式。第一种方式是直接赋值,第二种方式是浅拷贝,第三种是深拷贝。

直接赋值:在Java中,A a1 = a2,我们需要理解的是这实际上复制的是引用,也就是说a1和a2指向的是同一个对象。因此,当a1变化的时候,a2里面的成员变量也会跟着变化。

/* 建立类 */  
class Resume {  
    private String name;  //姓名  
    private String sex;   //性别  
    private int age;      //年龄  
    private String experience; //工作经历  
      
    public Resume(String name, String sex, int age) {  
        this.name = name;  
        this.sex = sex;  
        this.age = age;  
    }  
      
    public void setAge(int age) {  
        this.age = age;  
    }  
    public int getAge() {  
        return age;  
    }  
      
    public void setExperience(String experience) {  
        this.experience = experience;  
    }  
    public String getExperience() {  
        return experience;  
    }  
      
    public void displayResume() {  
        System.out.println("姓名:"+name+" 性别:"+sex+" 年龄:"+age);  
        System.out.println("工作经历:"+experience);  
    }  
}  
  
public class MainClass {  
    public static void main(String[] args) {  
        Resume zhangsan = new Resume("zhangsan","男",24);  
        zhangsan.setExperience("2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等代码复制");  
        zhangsan.displayResume();  
        Resume zhangsan1 = zhangsan;  
        zhangsan1.setExperience("2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等");  
        zhangsan.displayResume();  
        zhangsan1.displayResume();  
    }  
}

姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等代码复制  
姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等  
姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等

浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。实现上面想法之一的方法就是Object的Clone()函数。

比如我们希望把a1赋值给a2之后,a1和a2能保持独立,不要互相影响。

/* 建立类,实现Clone方法  */  
class Resume  implements Cloneable{  
    private String name;  //姓名  
    private String sex;   //性别  
    private int age;      //年龄  
    private String experience; //工作经历  
      
    public Resume(String name, String sex, int age) {  
        this.name = name;  
        this.sex = sex;  
        this.age = age;  
    }  
      
    public void setAge(int age) {  
        this.age = age;  
    }  
    public int getAge() {  
        return age;  
    }  
      
    public void setExperience(String experience) {  
        this.experience = experience;  
    }  
    public String getExperience() {  
        return experience;  
    }  
      
    public void displayResume() {  
        System.out.println("姓名:"+name+" 性别:"+sex+" 年龄:"+age);  
        System.out.println("工作经历:"+experience);  
    }  
      
    public Object clone() {  
        try {  
            return (Resume)super.clone();  
        } catch (Exception e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  
}  
  
public class MainClass {  
    public static void main(String[] args) {  
        Resume zhangsan = new Resume("zhangsan","男",24);  
        zhangsan.setExperience("2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等代码拷贝和粘贴");  
        zhangsan.displayResume();  
        Resume zhangsan1 = (Resume)zhangsan.clone();  
        zhangsan1.setAge(23);  
        zhangsan1.displayResume();  
        Resume zhangsan2 = (Resume)zhangsan.clone();  
        zhangsan2.setExperience("2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等代码");  
        zhangsan2.displayResume();  
        zhangsan.displayResume();  
    }  
}

姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等代码拷贝和粘贴  
姓名:zhangsan 性别:男 年龄:23  
工作经历:2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等代码拷贝和粘贴  
姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等代码  
姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学,精通JAVA,C,C++,C#等代码拷贝和粘贴

由程序的运行结果可以看出,我们实现了a1和a2引用的独立。但是什么叫“如果该字段是引用类型的话,则复制引用但不复制引用的对象。参考如下代码:

class Experience {  
      
    private String educationBackground;  
    private String skills;  
      
    public void setExperience(String educationBackground, String skills) {  
        // TODO Auto-generated constructor stub  
        this.educationBackground = educationBackground;  
        this.skills = skills;  
    }  
    public String toString() {  
        return educationBackground + skills;  
    }  
}  
  
/* 建立类,实现Clone方法  */  
class Resume  implements Cloneable{  
    private String name;  //姓名  
    private String sex;   //性别  
    private int age;      //年龄  
    private Experience experience; //工作经历  
      
    public Resume(String name, String sex, int age) {  
        this.name = name;  
        this.sex = sex;  
        this.age = age;  
        this.experience = new Experience();  
    }  
      
    public void setAge(int age) {  
        this.age = age;  
    }  
    public int getAge() {  
        return age;  
    }  
      
    public Experience getExperience() {  
        return experience;  
    }  
      
    public void setExperience(String educationBackground, String skills) {  
        experience.setExperience(educationBackground, skills);  
    }  
      
    public void displayResume() {  
        System.out.println("姓名:"+name+" 性别:"+sex+" 年龄:"+age);  
        System.out.println("工作经历:"+experience.toString());  
    }  
      
    public Object clone() {  
        try {  
            return (Resume)super.clone();  
        } catch (Exception e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  
}  
  
public class MainClass {  
    public static void main(String[] args) {  
        Resume zhangsan = new Resume("zhangsan","男",24);  
        zhangsan.setExperience("2009-2013就读于家里蹲大学","精通JAVA,C,C++,C#等代码拷贝和粘贴");  
        zhangsan.displayResume();  
  
        Resume zhangsan2 = (Resume)zhangsan.clone();  
        zhangsan2.setExperience("2009-2013就读于家里蹲大学","精通JAVA,C,C++,C#等");  
        zhangsan2.displayResume();  
        zhangsan.displayResume();  
        zhangsan2.displayResume();  
    }  
}

姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学精通JAVA,C,C++,C#等代码拷贝和粘贴  
姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学精通JAVA,C,C++,C#等  
姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学精通JAVA,C,C++,C#等  
姓名:zhangsan 性别:男 年龄:24  
工作经历:2009-2013就读于家里蹲大学精通JAVA,C,C++,C#等

第一段程序的工作经历是作为Resume类的一个普通的成员变量,也就是值属性。而后面一段程序中,工作经历Experience是一个类。结合上面程序的运行结果,我们再来理解“如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

深拷贝基本规则如下:

  • 基本类型:如果变量是基本很类型,则拷贝其值,比如int、float等。
  • 对象: 如果变量是一个实例对象,则拷贝其地址引用,也就是说此时新对象与原来对象是公用该实例变量。
  • String字符串:若变量为String字符串,则拷贝其地址引用。但是在修改时,它会从字符串池中重新生成一个新的字符串,原有紫都城对象保持不变。

如何利用序列化来完成对象的拷贝呢?在内存中通过字节流的拷贝是比较容易实现的。把母对象写入到一个字节流中,再从字节流中将其读出来,这样就可以创建一个新的对象了,并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。

public class CloneUtils {
    @SuppressWarnings("unchecked")
    public static  T clone(T obj){
        T cloneObj = null;
        try {
            //写入字节流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();
            
            //分配内存,写入原始对象,生成新对象
            ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新对象
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

使用该工具类的对象必须要实现Serializable接口,否则是没有办法实现克隆的。事实上使用该工具类的对象只要实现Serializable接口就可实现对象的克隆,无须继承Cloneable接口实现clone()方法。

public class Person implements Serializable{
    private static final long serialVersionUID = 2631590509760908280L;

    ..................
    //去除clone()方法

}

public class Email implements Serializable{
    private static final long serialVersionUID = 1267293988171991494L;
    
    ....................
}
public class Client {
    public static void main(String[] args) {
        //写封邮件
        Email email = new Email("请参加会议","请与今天12:30到二会议室参加会议...");
        
        Person person1 =  new Person("张三",email);
        
        Person person2 =  CloneUtils.clone(person1);
        person2.setName("李四");
        Person person3 =  CloneUtils.clone(person1);
        person3.setName("王五");
        person1.getEmail().setContent("请与今天12:00到二会议室参加会议...");
        
        System.out.println(person1.getName() + "的邮件内容是:" + person1.getEmail().getContent());
        System.out.println(person2.getName() + "的邮件内容是:" + person2.getEmail().getContent());
        System.out.println(person3.getName() + "的邮件内容是:" + person3.getEmail().getContent());
    }
}
-------------------
Output:
张三的邮件内容是:请与今天12:00到二会议室参加会议...
李四的邮件内容是:请与今天12:30到二会议室参加会议...
王五的邮件内容是:请与今天12:30到二会议室参加会议...

事实上org.apache.http.client.utils是基于反射实现的深拷贝:

@Immutable
public class CloneUtils {
    public static  T cloneObject(T obj) throws CloneNotSupportedException {
        if (obj == null) {
            return null;
        } else if (obj instanceof Cloneable) {
            Class clazz = obj.getClass();

            Method m;
            try {
                m = clazz.getMethod("clone", (Class[])null);
            } catch (NoSuchMethodException var5) {
                throw new NoSuchMethodError(var5.getMessage());
            }

            try {
                T result = m.invoke(obj, (Object[])null);
                return result;
            } catch (InvocationTargetException var6) {
                Throwable cause = var6.getCause();
                if (cause instanceof CloneNotSupportedException) {
                    throw (CloneNotSupportedException)cause;
                } else {
                    throw new Error("Unexpected exception", cause);
                }
            } catch (IllegalAccessException var7) {
                throw new IllegalAccessError(var7.getMessage());
            }
        } else {
            throw new CloneNotSupportedException();
        }
    }

    public static Object clone(Object obj) throws CloneNotSupportedException {
        return cloneObject(obj);
    }

    private CloneUtils() {
    }
}

 

threadlocal

基本结构

ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

java 基础(一)_第3张图片

由上图可知:

  • 每个Thread线程内部都有一个Map。
  • Map里面存储线程本地对象(key)和线程的变量副本(value)
  • 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

Thread线程内部的Map在类中描述如下:

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

基本方法 

ThreadLocal类提供如下几个核心方法:

  • get()方法用于获取当前线程的副本变量值。
  • set()方法用于保存当前线程的副本变量值。
  • initialValue()为当前线程初始副本变量值。
  • remove()方法移除当前前程的副本变量值。
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

步骤:

  1. 获取当前线程的ThreadLocalMap对象threadLocals
  2. 从map中获取线程存储的K-V Entry节点。
  3. 从Entry节点获取存储的Value副本值返回。
  4. map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set步骤:

  1. 获取当前线程的成员变量map
  2. map非空,则重新将ThreadLocal和新的value副本放入到map中。
  3. map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。 

ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。

java 基础(一)_第4张图片

Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。

static class Entry extends WeakReference {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

简单来说,ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

 

显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

ThreadLocalMap的问题

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

ThreadLocal threadLocal = new ThreadLocal();
try {
    threadLocal.set(new Session(1, "Misout的博客"));
    // 其它业务逻辑
} finally {
    threadLocal.remove();
}

应用场景

java 基础(一)_第5张图片 

 ThreadLocal适用于数据库连接管理,线程会话管理等场景。每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。

 总结:

  • 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
  • ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
  • 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。

基础数据

八种基本数据类型:int、short、float、double、long、boolean、byte、char。

封装类分别是:Integer、Short、Float、Double、Long、Boolean、Byte、Character。

包装类

Integer是int对应的包装类,它有一个int类型的字段存储数据,并且提供了基本操作,比如数学运算、int和字符串之间转换等。在Java 5中,引入了自动装箱和自动拆箱功能(boxing/unboxing),Java可以根据上下文,自动进行转换,极大地简化了相关编程。
关于Integer的值缓存,这涉及Java 5中另一个改进。构建Integer对象的传统方式是直接调用构造器,直接new一个对象。但是根据实践,我们发现大部分数据操作都是集中在有限的、较小的数值范围,因而,在Java 5中新增了静态工厂方法valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照Javadoc,这个值默认缓存是-128到127之间。

这种缓存机制并不是只有Integer才有,同样存在于其他的一些包装类,比如:

  • Boolean,缓存了true/false对应实例,确切说,只会返回两个常量实例Boolean.TRUE/FALSE。
  • Short,同样是缓存了-128到127之间的数值。
  • Byte,数值有限,所以全部都被缓存。
  • Character,缓存范围'\u0000' 到 '\u007F'。

字符串是不可变的,保证了基本的信息安全和并发编程中的线程安全。如果你去看包装类里存储数值的成员变量“value”,你会发现,不管是Integer还Boolean等,都被声明为“private fnal”,所以,它们同样是不可变类型!

自动装箱/自动拆箱

编译阶段、运行时,自动装箱/自动拆箱是发生在什么阶段?

自动装箱实际上算是一种语法糖。什么是语法糖?可以简单理解为Java平台为我们自动进行了一些转换,保证不同的写法在运行时等价,它们发生在编译阶段,也就是生成的字节码是一致的。看下面的示例:

//声明一个Integer对象,自动的装箱: 
Integer num = 10;	//Integer num = new Integer(10);
 
//自动拆箱 
Integer num = 10; 
System.out.print(num--);
 
//在-128~127 之外的数 
Integer num1 = 128; Integer num2 = 128; 
Integer num3 = 97; Integer num4 = 97; 
System.out.println("num1==num2: "+(num1==num2)); //false
System.out.println("num3==num4: "+(num3==num4)); //true

java定义:在自动装箱时对于值从–128到127之间的值(共享),它们被装箱为Integer对象后,会存在内存中被重用,始终只存在一个对象,而如果超过了从–128到127之间的值,被装箱后的Integer对象并不会被重用,即相当于每次装箱时都新建一个 Integer对象,使用了一种模式:叫享元模式(flyweight)。 

原始类型线程安全

  • 原始数据类型的变量,显然要使用并发相关手段,才能保证线程安全,这些我会在专栏后面的并发主题详细介绍。如果有线程安全的计算需要,建议考虑使用类似AtomicInteger、AtomicLong这样的线程安全类。
  • 特别的是,部分比较宽的数据类型,比如foat、double,甚至不能保证更新操作的原子性,可能出现程序读取到只更新了一半数据位的数值!

Java原始数据类型和引用类型局限性

  • 原始数据类型和Java泛型并不能配合使用,这是因为Java的泛型某种程度上可以算作伪泛型,它完全是一种编译期的技巧,Java编译期会自动将类型转换为对应的特定类型,这就决定了使用泛型,必须保证相应类型可以转换为Object。
  • 无法高效地表达数据,也不便于表达复杂的数据结构。比如我们知道Java的对象都是引用类型,如果是一个原始数据类型数组,它在内存里是一段连续的内存,而对象数组则不然,数据存储的是引用,对象往往是分散地存储在堆的不同位
    置。这种设计虽然带来了极大灵活性,但是也导致了数据操作的低效,尤其是无法充分利用现代CPU缓存机制。

字符串

String、StringBufer、StringBuilder

String是Java语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的Immutable类,被声明成为fnal class,所有属性也都是fnal的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的String对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBufer是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用append或者add方法,把字符串添加到已有序列的末尾或者指定位置。StringBufer本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是StringBuilder。

StringBuilder是Java 1.5中新增的,在能力上和StringBufer没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

线程安全性

String是Immutable类的典型实现,原生的保证了基础线程安全,因为你无法对它内部数据进行任何修改,这种便利甚至体现在拷贝构造函数中,由于不可变,Immutable对象在拷贝时不需要额外复制数据。

StringBufer的线程安全是通过把各种修改数据的方法都加上synchronized关键字实现的,非常直白。

StringBufer和StringBuilder底层都是利用可修改的(char,JDK 9以后是byte)数组,二者都继承了AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了synchronized。

另外,这个内部数组应该创建成多大的呢?如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy。

字符串缓存

String在Java 6以后提供了intern()方法,目的是提示JVM把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用intern()方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。一般来说,JVM会将所有的类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。

在jdk 6中,被缓存的字符串是存在所
谓PermGen里的,也就是臭名昭著的“永久代”,这个空间是很有限的,如果使用不当,OOM就会光顾。所以在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在JDK 8中被MetaSpace(元数据区)替代了。而且,默认缓存大小也在不断地扩大中,从最初的1009,到7u40以后被修改为60013。

Switch能否用string做参数

jdk7之前 switch 只能支持 byte、short、char、int 这几个基本数据类型和其对应的封装类型。

switch后面的括号里面只能放int类型的值,但由于byte,short,char类型,它们会?自动?转换为int类型(精精度小的向大的转化),所以它们也支持。jdk1.7后整形,枚举类型,boolean,字符串都可以。

public class TestString {
    static String string = "123";
    public static void main(String[] args) {
        switch (string) {
        case "123":
            System.out.println("123");
            break;
        case "abc":
            System.out.println("abc");
            break;
        default:
            System.out.println("defauls");
            break;
        }
    }
}

其实,jdk1.7并没有新的指令来处理switch string,而是通过调用switch中string.hashCode,将string转换为int从而进行判断。

IO

java IO方式

Java IO方式有很多种,基于不同的IO抽象模型和交互方式,可以进行简单区分。

  • 首先,传统的java.io包,它基于流模型实现,提供了我们最熟知的一些IO功能,比如File抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
  • 在Java 1.4中引入了NIO框架(java.nio包),提供了Channel、Selector、Bufer等新的抽象,可以构建多路复用的、同步非阻塞IO程序,同时提供了更接近操作系统底层的高性能数据操作方式。
  • 在Java 7中,NIO有了进一步的改进,也就是NIO 2,引入了异步非阻塞IO方式,也有很多人叫它AIO(Asynchronous IO)。异步IO操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。

同步IO分类

  • 按照流向不同,分为 输入流、输出流。输入流:只能从中读取数据,而不能向其写入数据。
    输出流:只能向其写入数据,而不能从中读取数据。
  • 按照流对象不同,分为 字节流、字符流
  • 按照处理过程不同,分为 节点流、处理流(一种典型的装饰器模式)

java 基础(一)_第6张图片

字节流和字符流

字节流和字符流和用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同。
字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。字节流和字符流的区别:

  • 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
  • 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。

InputStream

InputStream 是所有的输入字节流的父类,它是一个抽象类,主要包含三个方法:

//读取一个字节并以整数的形式返回(0~255),如果返回-1已到输入流的末尾。 
int read() ; 
//读取一系列字节并存储到一个数组buffer,返回实际读取的字节数,如果读取前已到输入流的末尾返回-1。 
int read(byte[] buffer) ; 
//读取length个字节并存储到一个字节数组buffer,从off位置开始存,最多len, 返回实际读取的字节数,如果读取前以到输入流的末尾返回-1。 
int read(byte[] buffer, int off, int len) ; 

Reader

Reader 是所有的输入字符流的父类,它是一个抽象类,主要包含三个方法: 

 

//读取一个字符并以整数的形式返回(0~255),如果返回-1已到输入流的末尾。 
int read() ; 
//读取一系列字符并存储到一个数组buffer,返回实际读取的字符数,如果读取前已到输入流的末尾返回-1。 
int read(char[] cbuf) ; 
//读取length个字符,并存储到一个数组buffer,从off位置开始存,最多读取len,返回实际读取的字符数,如果读取前以到输入流的末尾返回-1。 
int read(char[] cbuf, int off, int len) 

对比InputStream和Reader所提供的方法,就不难发现两个基类的功能基本一样的,只不过读取的数据单元不同。

在执行完流操作后,要调用close()方法来关系输入流,因为程序里打开的IO资源不属于内存资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源。

OutputStream

OutputStream 是所有的输出字节流的父类,它是一个抽象类,主要包含如下四个方法:

//向输出流中写入一个字节数据,该字节数据为参数b的低8位。 
void write(int b) ; 
//将一个字节类型的数组中的数据写入输出流。 
void write(byte[] b); 
//将一个字节类型的数组中的从指定位置(off)开始的,len个字节写入到输出流。 
void write(byte[] b, int off, int len); 
//将输出流中缓冲的数据全部写出到目的地。 
void flush();

 Writer 是所有的输出字符流的父类,它是一个抽象类,主要包含如下六个方法:

//向输出流中写入一个字符数据,该字节数据为参数b的低16位。 
void write(int c); 
//将一个字符类型的数组中的数据写入输出流, 
void write(char[] cbuf) 
//将一个字符类型的数组中的从指定位置(offset)开始的,length个字符写入到输出流。 
void write(char[] cbuf, int offset, int length); 
//将一个字符串中的字符写入到输出流。 
void write(String string); 
//将一个字符串从offset开始的length个字符写入到输出流。 
void write(String string, int offset, int length); 
//将输出流中缓冲的数据全部写出到目的地。 
void flush() 

 可以看出,Writer比OutputStream多出两个方法,主要是支持写入字符和字符串类型的数据。

 

使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,还能将输出流缓冲区的数据flush到物理节点里(因为在执行close()方法之前,自动执行输出流的flush()方法)

 RandomAccessFile

我们在对文件的操作过程中,除了使用字节流和字符流的方式之外,我们还可以使用RandomAcessFile这个工具类来实现。

RandomAccessFile可以实现对文件的,但是他并不是继承于以上4中基本虚拟类。

而且在对文件的操作中,RandomAccessFile有一个巨大的优势,他可以支持文件的随机访问,程序快可以直接跳转到文件的任意地方来读写数据。所以如果需要访问文件的部分内容,而不是把文件从头读到尾,使用RandomAccessFile将是更好的选择。

RandomAccessFile的方法虽然多,但它有一个最大的局限,就是只能读写文件,不能读写其他IO节点。

RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传。

java 基础(一)_第7张图片

"rws" 打开以便读取和写入,对于 "rw",还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
"rwd" 打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到底层存储设备。

读取文件:

RandomAccessFile raf = new RandomAccessFile(file,"r");
            String s = null;
            while ((s = raf.readLine())!=null){
                System.out.println(s);
            }
            raf.close();

写入文件:

String text = "写入的内容 \n";
            RandomAccessFile raf = new RandomAccessFile(file,"rw");
            raf.seek(12);   //改变写入偏移的位置,从地12个字节的位置开始写入
            raf.write(text.getBytes());
            raf.close();

 注意:RandomAccessFile虽然可以设置了偏移的方法,但他不能实现中间插入的效果,如果你需要实现文本中间插入的话,要先将后面的文件内容拷贝,然后写入,最后在写入的写一行,将拷贝的东西复制回来。

 

序列化

基本概念

把对象转换为字节序列的过程称为对象的序列化
把字节序列恢复为对象的过程称为对象的反序列化
对象的序列化主要有两种用途:

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  • 在网络上传送对象的字节序列。

  在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

  当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

JDK类库中的序列化API

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
  java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
  只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。
  对象序列化包括如下步骤:

  1. 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  2.  通过对象输出流的writeObject()方法写对象。

  对象反序列化的步骤如下:

  1. 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
  2. 通过对象输入流的readObject()方法读取对象。

示例如下所示,假设Person实现Serializable接口


public class TestObjSerializeAndDeserialize {

    public static void main(String[] args) throws Exception {
        SerializePerson();//序列化Person对象
        Person p = DeserializePerson();//反序列Perons对象
        System.out.println(MessageFormat.format("name={0},age={1},sex={2}",
                                                 p.getName(), p.getAge(), p.getSex()));
    }
    
    
    private static void SerializePerson() throws FileNotFoundException,
            IOException {
        Person person = new Person();
        person.setName("gacl");
        person.setAge(25);
        person.setSex("男");
        // ObjectOutputStream 对象输出流,将Person对象存储到E盘的Person.txt文件中,完成对Person对象的序列化操作
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
                new File("E:/Person.txt")));
        oo.writeObject(person);
        System.out.println("Person对象序列化成功!");
        oo.close();
    }

 
    private static Person DeserializePerson() throws Exception, IOException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
                new File("E:/Person.txt")));
        Person person = (Person) ois.readObject();
        System.out.println("Person对象反序列化成功!");
        return person;
    }

}

序列化Person成功后在E盘生成了一个Person.txt文件,而反序列化Person是读取E盘的Person.txt后生成了一个Person对象。

serialVersionUID的作用

s​e​r​i​a​l​V​e​r​s​i​o​n​U​I​D​:​ ​字​面​意​思​上​是​序​列​化​的​版​本​号​,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。

java序列化是通过在运行时判断serialVersionUID来验证版本的一致性。在进行反序列化时,JVM会把传过来的字节流中serialVersionUID与本地相应的实体(类)的serialVersionUID进行对比, 如果相同则是认为一致的,否则就会抛出异常InvalidClassException。

serialVersionUID有两种生成方式:默认生成和显示指定。

默认UID的值来源于类的几个方面:类名(class name)、类及其属性的修饰符(class modifiers)、 接口及接口顺序(interfaces)、属性(fields)、静态初始化(static initializer), 构造器(constructors)。也就是说这其中任何一个的改变都会影响UID的值,导致不兼容性。

显示方式使用情景:对类有一定的向下兼容性(稍后将具体分析哪些情况兼容),当不允许兼容时,可以通过改变UID的值在实现。强烈建议使用显示指定的方式,以防范潜在的不兼容根源,且可以带来小小的性能提升。

jdk自带的序列化的不足

jdk自带的序列化工具有以下缺点:

  1. 无法跨语言。这应该是java序列化最致命的问题了。由于java序列化是java内部私有的协议,其他语言不支持,导致别的语言无法反序列化,这严重阻碍了它的应用。
  2. 序列后的码流太大。java序列化的大小是二进制编码的5倍多!
  3. 序列化性能太低。java序列化的性能只有二进制编码的6.17倍,可见java序列化性能实在太差了。

因此可以采用Protobuf,hessian等序列化包,也可以序列化成json。参考文档:https://www.jianshu.com/p/937883b6b2e

参考文档:

https://www.cnblogs.com/ygj0930/p/6543877.html

http://www.importnew.com/12399.html

https://www.cnblogs.com/JohnTsai/p/5598036.html

https://www.cnblogs.com/liun1994/p/6691094.html

http://blog.csdn.net/seu_calvin/article/details/52094115

https://www.cnblogs.com/qlky/p/7348353.html

https://www.jianshu.com/p/98b68c97df9b

https://www.cnblogs.com/xdp-gacl/p/3777987.html

https://www.jianshu.com/p/2ba8e4991cff

https://www.jianshu.com/p/aea76bc0e6d1

https://www.jianshu.com/p/80f8a74d4662

你可能感兴趣的:(JAVA基础)