java基础

Posted by Liber Sun on September 28, 2018

三大特性

1.封装,封装隐藏细节 对外暴露 属性与操作。

2.继承,单继承

3.多态,重写、重载。

对象的深拷贝和浅拷贝

拷贝是把一个对象的所有属性拷贝到另一个对象中去,其目的再与复用现有的对象,但是又不能改变现有对象。

被拷贝对象 A 拷贝对象 a

浅拷贝:对于基本数据类型的成员变量,会进行值的传递,因此 a和A的操作互不影响;对于引用类型(数组,对象)会进行引用传递,也就是进行内存地址的复制,在这种情况下 a和A 会互相影响。

深拷贝:不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!(序列化实现深拷贝最方便,但是transient修饰的字段不能被序列化;比较传统的复写clone方法工作量太大)

异常和错误

异常实现程序的健壮性

Java标准库中建了一些通用的异常,这些类继承于Throwable为顶层父类

Throwable又派生出Error类和Exception类。

Error:代表了JVM本身的错误,这些错误不能被程序员通过代码进行处理,Error很少出现且难以处理。

Exception:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。

error

另外我们可以根据JavaC对于异常的处理要求,将Throwable分为另类:

  • 非检查异常:Error以及RuntimeException,这些异常,在javac编译时,并不会提示与发现这些异常。当然我们也不建议通过异常获取去处理这些错误。因为这些错误往往是代码本身就存在问题,我们应该修改代码以避免错误。
  • 检查异常:JavaC强制要求为此类异常做异常捕获与处理,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。

那么我们如何对异常进行处理呢?有两种方法 一种方法是利用try..catch..finally自身进行处理,另一种是throws异常声明 交给函数调用者caller去解决。

反射(降耦合)

反射是框架设计的灵魂,反射避免了写死这种操作,降低了程序的耦合性,给了程序更大的灵活性。

每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。

类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName(“com.mysql.jdbc.Driver”) 这种方式来控制类的加载,该方法会返回一个 Class 对象。

Java的反射机制是在运行状态中,你都可以获取任意一个类,任意一个对象,并对它进行操作。

Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:

  • Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
  • Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
  • Constructor :可以用 Constructor 的 newInstance() 创建新的对象。
Class stuClass3 = Class.forName("fanshe.Student");

Java反射的核心在于,JVM在运行的时候才会动态的加载类,并调用其方法访问属性。它不需要预先知道运行对象是谁,完全取决于Class.forName("类型")中定义的类型字符串。

反射的常见应用场景包括:

  • 加载数据库驱动(不同的数据库厂商提供的JDBC)
  • Spirng的IOC容器,通过XML配置或者注解的方式来动态创建类
  • 工厂方法
  • 通过反射获取不知道类型类的字段,进行序列化和反序列化

尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。

  • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
  • 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。
  • 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

泛型(提高重用性)

本质是,将类型由原来的具体 变成 类型参数化 ,提高代码的重用性,

public class Box<T> {
    // T stands for "Type"
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

泛型的形式包括 <T> <E> <K> <V> <T extends Number> <T super Number> 用于声明 类(类名之后),方法(方法返回值之前), <?> <? extends Number>(Number的子类都可以) <? super Number> 通配符用于参数 在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型,当然,在逻辑上我们可以理解成多个不同的泛型类型。

泛型只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,然后利用强制转换设置对应的类型,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。

泛型信息不会进入到运行时阶段。

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

同时是没有 泛型数组 这一定义,但是用通配符是可以的

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
List<String>[] lsa = new List<String>[10]; //error,因为String会被察除

注意点

1.当泛型遇到重载,下面两个方法会出现混淆。因为String与Integer会被擦除,由此编译不通过。

    public  void method(List<String> list) {  
        System.out.println("invoke method(List<String> list)");  
    }  

    public  void method(List<Integer> list) {  
        System.out.println("invoke method(List<Integer> list)");  
    }  

2.当泛型遇到catch,同样会出错。

泛型的类型参数不能用在Java异常处理的catch语句中。

MyException<String>MyException<Integer>

注解

Java1.5引入了注解,java1.8扩大了注解的使用场景,以致于目前我们能在java代码的任何地方添加我们定义的注解。 在项目开发中,注解的使用无处不在。注解的使用简化了代码,减少了程序员的工作量。

@Target(ElementType.METHOD)

ElementType.TYPE
            FIELD
            METHOD
            PARAMETER
            CONSTRUCTOR
            LOCAL_VARIABLE
            ANNOTATION_TYPE
            PACKAGE
            TYPE_PARAMETER
            TYPE_USE
            MODULE

同时我们还可以声明注解的执行阶段。

@Retention(RetentionPolicy.RUNTIME)

RetentionPolicy.SOURCE 
               .CLASS
               .RUNTIME

反射包的Constructor类、Field类、Method类、Package类和Class类都实现了AnnotatedElement接口,我们可以通过反射类,从而获取经过注解描述的任何东西。

//声明Test注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

@Target用来约束注解可以应用的地方,基本上你能想到的所有地方都可以使用注解。

@Rentention用来约束注解的生命周期,分别有三个值,分别是源码级别、类文件级别和运行时注解,其含义如下:

SOURCE:注解将被编译器丢弃(该类型的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件里),如Lombok的@Data注解

CLASS:注解在class文件中可用,但会被VM丢弃(该类型的注解信息会保留在源码里和class文件里,在执行的时候,不会加载到虚拟机中),请注意,当注解未定义Retention值时,默认值是CLASS,如Java内置注解,@Override、@Deprecated、@SuppressWarnning等

RUNTIME:注解信息将在运行期(JVM)也保留,因此可以通过反射机制读取注解的信息(源码、class文件和执行的时候都有注解的信息),如SpringMvc中的@Controller、@Autowired、@RequestMapping等。

注解内部也可以定义其他元素,方便处理器使用。支持的元素类型包括:

  • 所有的基础类型(int,float,boolean,byte,double,char,long,short)
  • String
  • Class
  • enum
  • Annotation
  • 上述类型的数组

针对内部元素,我们必须有确定的值。也就是说。元素必须有default值,或者在使用注解时赋值。

Object

equals()

用于判断两个对象的地址是否相等(即 是否是同一个对象 if(this==obj) ),调用默认的equals()方法相当于使用==方法,判断内存地址,因此我们经常需要重写父类的equals()函数:如若两个对象的内容相等,equals()方法则返回true,反之返回false。

1. 对称性:如果x.equals(y)返回是"true",那么y.equals(x)也应该返回是"true"。
2. 反射性:x.equals(x)必须返回是"true"。
3. 类推性:如果x.equals(y)返回是"true",而且y.equals(z)返回是"true",那么z.equals(x)也应该返回是"true"。
4. 一致性:如果x.equals(y)返回是"true",只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是"true"。
5. 非空性,x.equals(null),永远返回是"false";x.equals(和x不同类型的对象)永远返回是"false"。

诸如String,Interger,Date等类的equals函数就被进行了覆盖。

Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y);      // false

那当我们@Override时,我们需要如此考虑。

我们注意在重写equals时,必须满足以下的要求:(其实我们在重写equals时,应当同时覆盖hashCode,以保证元素的唯一性)

  • 检查是否为同一个对象的引用,如果是直接返回 true;
  • 检查是否是同一个类型,如果不是,直接返回 false;
  • 将 Object 对象进行转型;
  • 判断每个关键域是否相等。
public class EqualExample {

    private int x;
    private int y;
    private int z;

    public EqualExample(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EqualExample that = (EqualExample) o;

        if (x != that.x) return false;
        if (y != that.y) return false;
        return z == that.z;
    }
}

hashCode()

用于获取对象在哈希表中的索引位置, 虽然,每个Java类都包含hashCode()函数。但是,仅仅当创建并某个“HashTable”(HashMap,Hashtable,HashSet)时,该类的hashCode()才有用。 也就是说:hashCode() 在散列表中才有用,在其它情况(线性表)下没用。 在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置,前提是散列表中

  • 如果两个对象相等,那么它们的hashCode()值一定要相同;
  • 如果两个对象hashCode()相等,它们并不一定equals()相等。

3.两者的关系

当不创建”类的散列表“的时候,hashCode()和equals()没有任何联系。

当创建”类的散列表“的时候,他们是由关系的:

  • 1)如果两个对象equals()相等,那么它们的hashCode()值一定要相同;
  • 2)如果两个对象hashCode()相等,它们并不一定equals()相等。 因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。
  • 3)如果两个对象hashCode()不相等,它们一定equals()不相等。 因此在往散列集合中添加元素时,可以先比较hashCode(),相等后再比较equals(),提高比较的效率。

toString()

默认返回 XXXClass@hashValue这种形式,@后面是hash值的无符号16进制。

那么我们应该如何覆写toString()方法,利用反射获取到 声明的变量 。

clone()

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

public class CloneExample {
    private int a;
    private int b;

    @Override
    public CloneExample clone() throws CloneNotSupportedException {
        return (CloneExample)super.clone();
    }
}

clone()又分为浅拷贝和深拷贝。

  • 浅拷贝:拷贝对象和原始对象的引用类型引用同一个对象。
  • 深拷贝:拷贝对象和原始对象的引用类型引用不同对象。

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。 Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象.

public class CloneExample {
    private int a;
    private int b;

    @Override
    public CloneExample clone() throws CloneNotSupportedException {
        return (CloneExample)super.clone();
    }

    public CloneExample() {}

    public CloneExample(CloneExample cloneExample) {
        this.a=cloneExample.a;
        this.b=cloneExample.b;
    }
}

包装类

所有的基本类型都存在对应的包装类。

自动装箱拆箱

基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。

Integer x = 2;     // 装箱
int y = x;         // 拆箱

缓存池

boolean : true and false all byte values short : -128 ~ 127 int : -128 ~ 127 char : \u0000 ~ \u007F

在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。

Integer m = 123;//   -128<123<127
Integer n = 123;
System.out.println(m == n); // true

Integer h = Integer.valueOf(128);// 128>127
Integer v = Integer.valueOf(128);
System.out.println(h == v);   // false
  • new Integer(123) 每次会创建新的对象
  • Integer.valueOf(123) 或者 123的形式 会使用缓存池中(缓存池Integer大小默认为-128~127)的对象,多次调用会取得同一个对象的引用
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false

Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true

在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界

关键字

static

static 静态的,可以用来修饰 方法,类以及变量。同时也可以利用static代码块来优化程序性能。 1) 静态方法 ,在类加载的时候就已经存在了,静态方法不依赖对象就可以直接使用,当然因为不依赖对象,在其内部就无法使用类的非静态变量和非静态方法。 2) 静态变量,静态变量被所有的对象所共享,在内存中只有一个副本,会在类的初次加载时初始化。 3) 静态代码块,静态语句块在类初始化时运行一次,通过控制代码运行顺序,以优化程序性能。

静态变量和静态代码块的执行顺序就是代码的先后顺序。

    static {
        x=100;
    }
    private static int x=10;// x=10

实例态变量和实例代码块(顺序执行多次,每次声明实例都会触发)的执行顺序就是代码的先后顺序。

顺序:静态内容(静态变量和静态代码块,运行一次)->实例内容(实例变量和构造代码块,每次实例化都会执行)->构造方法

 父类的静态内容->子类的静态内容->父类的实例内容->父类的构造方法->子类的实例内容->子类的构造方法
class Work {
    private static int start;
    private static int end;
    private String name;//实例变量
    static {//静态代码块,只会执行一次,常用于类 属性的初始化(只用于静态变量)
        start=1;
        end=2;
    }

    {//构造代码块,每次 声明对象时调用, 优先于构造函数执行
        name="Type";
    }

    public Work(int start, int end) {
        this.start = start;
        this.end = end;
    }
}

1) 静态内部类 可以在类的静态方法中直接使用,而不依赖于外部类的实例对象。 静态内部类不能访问外部类的非静态的变量和方法。

   public class OuterClass {

    class InnerClass {
    }

    static class StaticInnerClass {
    }

    public static void main(String[] args) {
        // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        StaticInnerClass staticInnerClass = new StaticInnerClass();
    }
}

final

final 无法改变的,你将无法改变这个引用,它可以用于修饰变量、方法和类。

1)final变量

必须在声明的时候初始化或者在构造函数中初始化,否则会编译错误,并且不允许对其修改。

对于基本类型,final使其数值不变。 对于集合对象为final类型时,我们可以向其添加,删除以及修改其内容, 但是不能改变它的引用 对于类对象为final类型时,我们可以使用其setter/getter, 但是不能改变它的引用

final int x=1;
// x=2; // cannot assign value to final variable 'x'
final String y=new String();
// y=null; // cannot assign value to final variable 'x'
final List z=new ArrayList<>();
z.add("1");
//z=null;  // cannot assign value to final variable 'x'

2)final方法 fianl方法标志着不可被子类重写。 final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。 其实父类的private方法隐式地被指定为 final。

3)final类 final类的功能是完整的,它不能被继承。java中具有很多final类,比如String、Interger等。

4)final + static一起使用,用来声明一个 只读常量 ,其应该大写拼音。

接口和抽象类

不管是接口还是抽象类,都可以通过匿名内部类的方式访问。 但是注意,不能通过抽象类和接口直接创建对象,我们可以这样理解:一个内部类实现了接口里的抽象方法并且返回一个内部类对象,之后我们让接口的引用来指向这个对象。

抽象类

抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。

抽象类用于描述对象的行为,比如动物,动物会吃东西,但是动物具体会有是什么样的吃的行为,只有具体的动物类,如Dog,Cat才会有具体的行为。 抽象类是实现多态的一种机制。

Animal animal=new Animal();//不能实例化
Animal cat=new Cat();
Animal dog=new Dog();
cat.eat();
dog.eat();

1.抽象类不可以被实列化,需要继承抽象类才能实例化其子类。 2.抽象类的抽象方法必须被子类实现,非抽象方法自身实现 3.子类的抽象方法不可以和父类的抽象方法同名 4.子类不需要实现父类的所有方法(可以不实现父类的非抽象方法)

接口类

接口是通过interface来实现的,因此也是不能实例化的。

在java8之前,接口是不能有任何方法实现,但是从java8开始,接口就拥有了default,static方法。

当然为什么接口可以使用default方法?一个接口想要添加一个新的方法,不需要对实现类做任何修改;在java8之前,我们需要去修改所有的实现类。 注意这也是和Java8提出的函数式接口相对应的,利用接口的default方法为接口添加新的可以接受FunctionalInterface参数的方法,使它们更便于以函数式风格使用。

接口的实现类必须实现接口中定义的方法(除了接口定义的default或者static方法),java不允许多继承,但可以实现多个接口。

可以通过instanceOf来判别一个类是否实现了某个接口,if(obj instanceOf className) {doSomething()}

接口的所有字段默认都是public static final的。

区别和联系

语法层次

抽象类的定义:

public abstract class Demo
{
    private String name="demo";//抽象类可以拥有任意范围的成员数据,
    abstract void foo1();
    void foo2(){
        //实现
    }
}

接口的定义:

interface Demo
{
    final static  String name="demo";
    void foo1();
    default void foo2(){
        //实现
    }
    static void foo3(){
         //实现
    }
}

总结

  • 抽象类 可以拥有多种访问权限 字段可以是任意的
  • 接口 只能是public 字段都是final static

设计层次

从设计的层面来说,具有如下的不同点

1.抽象层次不同。抽象类是对类的抽象,接口是对行为的抽象。抽象类是是对类整体进行抽象,包括属性和行为。但是接口只是对类的局部(行为)进行抽象。

2.应用情景不同。抽象类描述的都是具有相似特点的类,而接口可以横跨不同的类。

抽象类包含着继承关系,说明了父类和派生类有一定的关系,及(派生类 is a kind of 抽象类),那么就必须满足里氏替换原则,所有父类出现的地方都可以用任意一个子类进行替换。

接口是一种 实现类 like 接口,你实现了这种接口,你就拥有了这个接口定义的方法。

使用选择

使用接口:

  • 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
  • 需要使用多重继承。

使用抽象类:

  • 需要在几个相关的类中共享代码。
  • 需要能控制继承来的成员的访问权限,而不是都为 public。
  • 需要继承非static和非final字段。

在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。

参数的传递

基本类型(byte,short,int,long,double,float,char,boolean)为传值(复制实体)。

对象类型(对象,数组,容器)为 转值(把对象引用的地址当作值传递,但是这个值实际是对象的引用)。

对于不可变的类 如String和包装类,

向下转型

Java 不能隐式执行向下转型,因为这会使得精度降低。 及精度高的不能转为精度低的。(double->float,long->int)

float f=1.1;//报错,因为 double的精度比float精度高
float f=1.1f;//成功。

隐式类型转换

short s=1;
s=s+1;  //会报错,因为int->short 不允许
s= (short) (s+1);
s+=1//成功 等价于  s= (short) (s+1);
s++;//成功   等价于 s= (short) (s+1);

内部类

内部类的表现形式为一个类可以在另一个类的内部存在,其中,内部包含其它类的类称为外部类,被包含的类称为内部类。 内部类在编译之后会形成两个文件 Outer.classOuter$Inner.class

成员内部类

此时的内部类就是外部类的一个普通变量,内部类可以访问外部类的所有属性。 外部类无法获取内部类的属性与方法,只能通过内部类对象来使用。 内部类不能存在static的内容。

静态内部类

具备static修饰的内部类,相当于是外部类的静态变量。 静态内部类的内部不能直接访问外部类的非静态成员,只能通过实例化外部类的对象,然后通过该对象进行访问。

局部内部类

局部内部类是出现在外部类的方法或者某个代码块的作用域范围内的内部类。(方法之类) 局部内部类仅在作用域范围内有效,超出范围则不再可使用。 局部内部类中可以使用方法或者代码块级别的变量,但是要求它们必须是final类型的常量。

匿名内部类

匿名内部类没有类名,通常被设计为只使用一次,用以简化代码的书写。 匿名内部类必须继承一个父类或者实现一个接口。 在匿名内部类中需要使用外部类的方法的属性时,要求该属性必须为final类型的常量。 在匿名内部类中需要使用外部类的方法的形参时,要求该形参必须为final类型的常量。

枚举

枚举其本质依然是类,但是却在类的基础删添加了特殊的约束,而这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。

enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

具体是如何实现的类:我们在使用关键字enum创建枚举之后,编译器会将该枚举编译成java.lang.Enum类。