JAVA 基础

抽象类和普通类的区别

先来看看什么是 抽象类

  • 抽象类:含有抽象方法的类叫作抽象类
  • 抽象方法:必须用"abstract"关键字进行修饰。抽象方法是一种特殊的方法,它只有声明,而没有具体的实现
[public] abstract class ClassName { //抽象类
    abstract void fun();//抽象方法
}
  • 因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象
  • 抽象类就是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情。
  • 对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为abstract方法,此时这个类也就成为abstract类了。

因此抽象类和普通类的区别为

  • 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。
  • 抽象类不能用来创建对象;
  • 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。

接口和抽象类的区别

先看看 接口 的定义:

  • 接口里面只有常量和抽象方法,也就是说,接口中的方法必须都是抽象方法
  • 接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰会报编译错误)
  • 方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、 final等修饰会报编译错误)
class ClassName implements Interface1,Interface2,[....]{
}//允许一个类实现多个接口

因此接口和抽象类的区别为

语法层面:

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
  • 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
  • 一个类只能继承一个抽象类,却可以实现多个接口
  • 如果一个非抽象类遵循了某个接口,就必须实现该接口中的所有方法。对于遵循某个接口的抽象类,可以不实现该接口中的抽象方法

设计层面:

  • 抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
  • 抽象类是一种模板式设计。而接口是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动

继承、重写,重载,多态

继承

Java中继承是通过extends来实现,

  • 子类拥有父类非private的属性和方法
  • 子类可以拥有自己属性和方法,即子类可以对父类进行扩展
  • 子类可以用自己的方式实现父类的方法(重载和重写)
public class Person {
    protected String name;
    protected int age;
    protected String sex;    
    Person(){ //构造器
        System.out.println("Person Constrctor...");
    }
}

public class Husband extends Person{
    private Wife wife;
    Husband(){
        System.out.println("Husband Constructor...");
    }    
    public static void main(String[] args) {
        Husband husband  = new Husband();
    }
}
#输出:
Person Constrctor...
Husband Constructor...

Note: 子类会自动调用父类的默认构造器,但当父类没有默认构造器,或者子类想调用父类的非默认构造器,需要显示得用super()来调用。

例如:

public class Person {
    protected String name;
    protected int age;
    protected String sex;    
    Person(String name){
        System.out.println("Person Constrctor-----" + name);
    }
}

public class Husband extends Person{
    private Wife wife;
    Husband(){
        super("chenssy");//显示调用父类构造器
        System.out.println("Husband Constructor...");
    }    
    public static void main(String[] args) {
        Husband husband  = new Husband();
    }
}
#输出:
Person Constrctor-----chenssy
Husband Constructor...

重写

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 要求:

  1. 子类方法参数列表与父类方法的参数列表必须完全相同。
  2. 子类方法的返回类型必须是父类方法返回类型或其子类型。
  3. 子类方法的访问权限必须大于等于父类方法。
  4. 子类方法抛出的异常类型必须是父类抛出异常类型或其子类型。
  5. 父类被static,private,final修饰的方法不能被重写。

Note: 使用@Override 注解,可以让编译器帮忙检查是否满足上面的几个限制条件

一个例子:

class SuperClass {
	protected List<Integer> func() throws Throwable {
 		return new ArrayList<>();
	}
}
class SubClass extends SuperClass {
	@Override
	public ArrayList<Integer> func() throws Exception {
		return new ArrayList<>();
	}
}

重载

重载是在一个类(包括本类和子类)里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。最常用的地方就是构造器的重载。

Note: 只有返回值不同,但参数列表相同的,不算重载。重载要求参数类型,个数,顺序至少有一个不同

重写与重载的区别

  • 重写 是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • 重载 是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • 重写是子类与父类的一种多态性表现,重载是一个类的多态性表现。

多态

多态的形式:
父类引用指向子类对象:Parent p = new Child ( ) ; 父类引用指向子类对象:\text{Parent p} = \text{new Child}(); 父类引用指向子类对象:Parent p=new Child();

多态的意义:在继承中我们知道子类是父类的扩展,它可以提供比父类更加强大的功能,如果我们定义了一个指向子类的父类引用类型,那么它除了能够引用父类的共性外,还可以使用子类强大的功能。

多态的必要条件

  • 继承:在多态中必须存在有继承关系的子类和父类
  • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法
  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法

多态的调用优先级
this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O) \text{this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)} this.show(O)super.show(O)this.show((super)O)super.show((super)O)

举例来说:

public class A {
    public String show(A obj) {
        return ("A and A");
    } 
}

public class B extends A{
    public String show(B obj){
        return ("B and B");
    }  
    public String show(A obj){
        return ("B and A");
    } 
}

public class C extends B{
}

public class Test {
    public static void main(String[] args) {
        A a2 = new B();
        B b = new B();
        C c = new C();
        
        System.out.println("4--" + a2.show(b));
        System.out.println("5--" + a2.show(c));
        System.out.println("7--" + b.show(b));
        System.out.println("8--" + b.show(c));    
    }
}
#结果是:
4--B and A
5--B and A
7--B and B
8--B and B

a2.show(b)为例,调用优先级为:
this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O) \text{this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)} this.show(O)super.show(O)this.show((super)O)super.show((super)O)

这里A a2 = new B(),B b = new B(),因此this是A,O是B。

  • 首先看this.show(O),也就是找A里有没有以B为输入的show方法,发现没有
  • 再看super.show(O),但A没有超类(父类)
  • 因此看this.show((super)O),B的父类是A,因此相当于看this.show(A),这时找到了A里有以A为输入的show方法,这里要注意,不是直接就调用A的show方法了,而是要先看看B里面有没有重写这个方法,然后发现B里面确实重写了这个方法,因此这里应该是用B里的以A为输入的show方法,所以输出是"B and A"

再看一例b.show(c),这时this是B,O是C。

  1. 首先看this.show(O),发现B里没有以C为输入的show方法
  2. 再看super.show(O),B的父类是A,A里也没有以C为输入的show方法
  3. 因此看this.show((super)O),C的父类是B,A,因此相当于看this.show(B,A),发现B里面既有以B为输入的show方法,也有以A为输入的show方法,但是C的直接父类是B,A是C父类的父类,因此这里优先用C的直接父类B,所以输出是"B and B"

public, protected, private

  1. private:
    可修饰数据成员,构造方法,方法成员,不能修饰类(此处指外部类)。被private修饰的成员,只能在定义它们的类内使用,其它类不能调用
  2. protected:
    可修饰数据成员,构造方法,方法成员,不能修饰类(此处指外部类)。被protected修饰的成员,能在定义它的类中,同package的类中被调用。如果有不同package的类想调用它,那么这个类必须是定义它的类的子类。
  3. public:
    可修饰类,数据成员,构造方法,方法成员。被public修饰的成员,可以在任何一个类中被调用,不管同package还是不同pakage

1. 为了代码安全起见,如果没有特殊需要,尽可能用 private。
2. 如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不得低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例去代替。

#########################################################################################

关键字

static关键字

static表示静态,从始至终只获得一块内存空间。一个子类改变了它,其他子类中的这个东西也会相应的被改变(因为引用的是同一个东西)

  • static修饰变量:修饰符必须 public。静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
  • static修饰方法:调用静态方法可以无需创建对象,直接使用"类名.方法名"的方法(反正不同对象引用的也是同一个东西);静态方法在访问本类的成员时,只允许访问静态成员,而不允许访问实例成员变量和实例方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。但对于非静态成员方法,它访问静态成员方法/变量显然是毫无限制的。我们最常见的static方法就是main方法,因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问
public class StaticBean {
  public static String A = "A";
  public String D;
  public static void getMessage(){
    System.out.println(A);
    System.out.println(D);//错误用法!!
  }
}
  • static修饰类:
  1. 只允许修饰内部类(写在类内的类),只能访问静态的成员变量和方法,不能访问非静态的方法和属性,但是普通内部类可以访问任意外部类的成员变量和方法
  2. 静态内部类可以声明普通成员变量和方法,而普通内部类不能声明静态成员变量和方法
  3. 静态内部类使用场景一般是当外部类需要使用内部类,而内部类无需外部类资源,并且内部类可以单独创建
  4. 静态内部类可以单独初始化: Inner i = new Outer.Inner();普通内部类初始化:Outer o = new Outer(); Inner i = o.new Inner();
  • 通过this访问static变量:
public class Main {  
    static int value = 33;
 
    public static void main(String[] args) throws Exception{
        new Main().printValue();
    }
 
    private void printValue(){
        int value = 3;
        System.out.println(this.value);
    }
}

this代表当前对象,那么通过new Main()来调用printValue的话,当前对象就是通过new Main()生成的对象。而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是33。在printValue方法内部的value是局部变量,根本不可能与this关联,所以输出结果是33。


final关键字

final关键字的作用是用来保证变量不可变。

  • final修饰变量:如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象,但是它指向的对象的内容是可变的
  • final修饰方法:把方法锁定,以防任何继承类修改它的含义;因此只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final类的private方法会隐式地被指定为final方法,如果在子类中定义的⽅法和基类中的一个private方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的⽅法。
  • 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));//true
        System.out.println((a == e));//false
    }
}

上面这段代码是因为:当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。而对于变量d的访问却需要在运行时通过链接来进行

但只有在编译期间能确切知道final变量值的情况下,编译器才会进行这样的优化,比如下面的这段代码就不会进行优化:

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));//false
 
    }
     
    public static String getHello() {
        return "hello";
    }
}

this关键字

  1. 当成员变量和局部变量重名时,在方法中使用this时,表示的是该方法所在类中的成员变量。(this是当前实例化对象)
public class Hello {
    String s = "Hello";
    public Hello(String s) {
       System.out.println("s = " + s);
       System.out.println("1 -> this.s = " + this.s);      
       this.s = s;//把参数值赋给成员变量,成员变量的值改变
       System.out.println("2 -> this.s = " + this.s);

    }
    public static void main(String[] args) {
       Hello x = new Hello("HelloWorld!");
       System.out.println("s=" + x.s);//验证成员变量值的改变    }
}
//输出结果为:
s = HelloWorld!
1 -> this.s = Hello
2 -> this.s = HelloWorld!
s=HelloWorld!

对于 static 修饰的方法而言,可以使用类来直接调用该方法,正因为 static 方法是独立于实例化对象之外的,而 this 关键字又依赖于实例化对象,因此 static 修饰的方法中不能使用 this 引用。

  1. this() 用来访问本类的构造方法,括号中可以有参数,如果有参数就是调用指定的有参构造方法。
public class ThisTest {
    private int age;
    private String str;
    ThisTest(String str) {
       this.str=str;
       System.out.println(str);
    }
    ThisTest(String str,int age) {
       this(str);//调用了本类中别的构造方法
       this.age=age;
       System.out.println(age);
    }
    public static void main(String[] args) {
       ThisTest thistest = new ThisTest("this测试成功",25);
    }
}
//输出为:
this测试成功
25

需要注意的是:
  1:在构造调用另一个构造函数,调用动作必须置于最起始的位置。
  2:不能在构造函数以外的任何函数内调用构造函数。
  3:在一个构造函数内只能调用一个构造函数。


super关键字

super()表示对父类对象的引用,主要有以下两种使用场景:

  1. 访问父类的构造函数:可以使用super()函数访问父类的构造函数,从而委托父类完成⼀些初始化的工作。应该注意到,子类⼀定会调用父类的构造函数来完成初始化工作,⼀般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用super()函数。
  2. 访问父类的成员方法和成员属性:如果子类重写了父类的某个方法,可以通过使⽤super关键字来引用父类的方法实现,使用方法是super.func(param)或者super.name

transient关键字

transients关键字是用来修饰成员变量的,被修饰的成员变量不参与序列化过程。

序列化:Java对象在电脑中是存于内存之中的,内存之中的存储方式毫无疑问和磁盘中的存储方式不同(一个显而易见的区别就是对象在内存中的存储分为堆和栈两部分,两部分之间还有指针;但是存到磁盘中肯定不可能带指针,一定是某种文本形式)。序列化和反序列化就是在这两种不同的数据结构之间做转化。

  • 序列化:JVM中的Java对象转化为字节序列。
  • 反序列化:字节序列转化为JVM中的Java对象。

一个例子,这里不希望String realName被序列化:

public class XiaoMei implements Serializable {
    private static final long serialVersionUID = -4575083234166325540L;

    private String nickName;
    private transient String realName;


    public XiaoMei(String nickName,String realName){
        this.nickName = nickName;
        this.realName = realName;
    }

    public String toString(){
        return String.format("XiaoMei.toString(): nickName=%s,realName=%s", nickName,realName);
    }
}

下面测试序列化前print内存中的对象,以及序列化后再重新读取该对象:

public class Test {
    public static void main(String[] args){
        String realName="王小美", nickName="王美美";
        XiaoMei x = new XiaoMei(nickName, realName);
        System.out.println("序列化前:"+x.toString());
        
        ObjectOutputStream outStream;
        ObjectInputStream inStream;
        String filePath = "/Users/jiangyoujun/Documents/test.log";
        try {
            outStream = new ObjectOutputStream(new FileOutputStream(filePath));
            outStream.writeObject(x);//序列化后存到本地文件

            inStream = new ObjectInputStream(new FileInputStream(filePath));
            XiaoMei readObject = (XiaoMei)inStream.readObject();//重新从本地文件读取
            System.out.println("序列化后:"+readObject.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出为:

序列化前:XiaoMei.toString(): nickName=王美美,realName=王小美
序列化后:XiaoMei.toString(): nickName=王美美,realName=null

这里,使用transient关键字修饰的成员变量没有被序列化。

在Java中,静态成员变量默认是不能被序列化的,因为它独立于对象存在,而序列化是针对对象状态的

使用场景

用户有一些敏感信息(譬如密码,银行卡号等),为了安全起见,不希望在网络操作中被传输或者保持。这些信息对应的变量就可以被定义为transient类型。换句话说,这个字段的生命周期仅存于调用者的内存中。


synchronized关键字(同步与异步)

使用synchronized关键字修饰一个方法的时候,该方法被声明为 同步 方法。这意味着当不同线程都需要调用 同一个实例对象 的这个方法时,同一时刻只能有一个线程访问该方法,需要等这个线程运行完该方法后,下一个线程才能访问。

多线程抢占执行现象
多线程并发执行的时候,会有抢占式执行的现象。例如:

public class SafeDemo {
    private static int i = 0;
    public void selfIncrement(){
        for(int j=0;j<50000;j++){
            i++;
        }
    }
    public int getI(){
        return i;
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        SafeDemo safeDemo = new SafeDemo();
        Thread t1 = new Thread(()->{
            safeDemo.selfIncrement();
        });
        Thread t2 = new Thread(()->{
            safeDemo.selfIncrement();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(safeDemo.getI()); // 9906
    }
}

上述代码本意是分别让count在线程1和线程2上各增加5万次,那么count最终的结果应该是10万,但实际的结果不一定是10万,甚至每次执行的结果都不一样。这是因为,在线程中进行一次自增的机器指令有三步:从主内存中把count值拿到cpu寄存器中->把寄存器中的count值进行自增1->把寄存器中的count值刷新到主内存中。虽然理想的情况是线程1先进行完上述三个步骤,将主内存的count刷新,线程2再加载刷新后的count,然后再做一次自增。但现实的情况有可能是,线程1还没来得及将主内存的count刷新,线程2就直接加载了主内存的count值,并执行一次自增。这样的话,虽然两个线程都执行了一次自增,但主内存的count值却只增加了1。因此上述程序的结果可以是5万到10万间的任意值

如何加锁
synchronized关键字就是用来解决上述问题的。被synchronized关键字修饰的方法(或者代码块),对于任意实例化对象,在执行到这个方法时,会先将对象锁住,在做完三个步骤,将主内存内的值刷新之后,再解锁该对象,这时其它线程才可以再对该对象上上锁,然后继续执行该方法。也就是说,保证每次只能有一个线程访问该方法,避免了多线程抢占执行的情况。对于上述例子,只需要使用 synchronized 修饰 selfIncrement() 方法即可:

public class SafeDemo {
    private static int i = 0;
    // 使用synchronized关键字进行保护
    public synchronized void selfIncrement(){
        for(int j=0;j<5000;j++){
            i++;
        }
    }
    public int getI(){
        return i;
    }
}

同步与异步

不管同步还是异步,都是针对对象的。 同步 是指多个线程需要对 同一个对象 进行操作,而 异步 就是指不同线程对 不同对象 进行操作,这时就不会出现上述所说由于多线程抢占执行导致的问题。

Note:尽管“加锁”是针对对象的,但是对于没有使用 synchronized 修饰的方法,线程还是可以正常调用的,不会因为带synchronized的方法没有执行完,就导致其它不带 synchronized 的方法也无法执行。

局部锁与全局锁

上述介绍的对普通方法使用 synchronized 修饰,上锁是针对对象的,称为局部锁。但是如果 synchronized 修饰的是静态方法,由于静态方法不依赖于对象,因此此时是全局锁。

#########################################################################################

字符串

String,StringBuilder和StringBuffer

String和StringBuilder

String由final关键词修饰,它是不可变的。如下代码:

String s="";
for(int i=0; i<time; i++){
    s += "java";
}

实际相当于是:

String s="";
for(int i=0; i<time; i++){
    StringBuilder sb = new StringBuilder(s);
    sb.append("java");
    s=sb.toString();
}

这个过程将不断的产生新的StringBuilder对象,并回收不需要的String和StringBuilder。如果涉及到大量修改string的需求,应该使用StringBuilder而不是String

StringBuilder和StringBuffer

StringBuilder不是多线程安全的,而StringBuffer是多线程安全的。因此如果涉及到多线程操作,应该用StringBuffer;否则就用StringBuilder

#########################################################################################

equals()和==

  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y);      // false,因为x和y各开辟了一块内存,然后指向这个内存空间

#########################################################################################

Java的值传递

首先下结论,java调用函数时的参数传递都是值传递,而不是引用传递。值传递就相当于把参数复制了一份传递给函数,那为什么依然会出现参数被改变的情形呢?看以下几个例子:

第一个例子:基本类型
void foo(int value) {
value = 100;
}
foo(num); // num 没有被改变

第二个例子:没有提供改变自身方法的引用类型
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”。

主要看看例3和例4为什么不一样,例3的情况是:

builder.append("4")之后:

sb本身没有变,但是builder.append("4")把它指向的内容改变了

再看例4,传递参数时是一样的:

但由于builder是重新new了一个StringBuilder,因此它开辟了一块新空间:

所以builder做的任何操作都不会影响到sb

以上就解释了,为什么都是值传递,但有的实参会受影响,而有的不会。

#########################################################################################

哈希

哈希(也称“散列”)就是把任意长度的输入,通过散列算法,变换成固定长度的输出。总体而言,哈希函数用于,将消息或数据压缩,生成数据摘要,最终使数据量变小,并拥有固定格式。

特点

  • 相同的输入一定得到相同的输出;
  • 不同的输入大概率得到不同的输出。

作用

  • 哈希使数据的存储与查询速度更快。
  • 由于无法根据输出得到输入,哈希能对信息进行加密处理,使得数据传播更为安全。

主要步骤

  1. 根据哈希函数取得哈希值
  2. 处理哈希冲突

各种hash函数构造法及常见处理冲突的方法详见: hash算法原理详解


链地址法

这里简单说一下 HashMap 中使用的冲突解决法:链地址法。

链地址法将所有哈希值相同的元素记录在同一线性单链表中,并称这种表为同义词子表,在哈希表中只存储所有同义词子表的头指针。对于集合{12,67,56,16,25,37,22,29,47,48,34},我们用12作为除数,进行除留余数法,可得到下图结构,此时,就不存在哈希冲突换地址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。

在这里插入图片描述
链地址法对于可能会造成很多冲突的哈希函数来说,提供了绝不会出现找不到地址的保障。当然,这也增加了查找时需要遍历单链表的性能消耗。


为什么哈希查找效率高

假设已有序列 {20,30,50,70,80},现在新进来一个元素 x,我们想知道这个元素是否存在于这个序列。naive的方法是逐个比较,那么查询复杂度是 O(n)。而使用哈希查找法,我们可以先构建一个哈希表,假设哈希函数是 f(x) = x/10,对于序列中的每一个元素,计算其哈希值作为在哈希表中的索引位置,而这个元素就放入该位置。那么哈希表如下:

0123456789
2030507080

这样,当进来一个新的元素 x,我们只需要计算它的哈希值 f(x),然后再对比它与哈希表对应位置的元素是否相同即可。一般来说,哈希查找的复杂度为 O(1)。

由于哈希冲突的存在,如果第一次计算的哈希位置上的元素不对,那么要根据处理哈希冲突的方法,再次计算新的哈希位置,直至走完哈希表,还没有遇到的话,就说明这个元素不存在于该序列


Java中的哈希

以HashMap为例,哈希发挥作用的地方主要在map.put(k,v)map.get(k)两个方法:

在这里插入图片描述
(上图来自 JAVA - 哈希表)

总结来说,都是先根据k值计算 hash(k),然后以 hash(k) 为索引找到该位置,如果该位置上已经有链表,就调用 equals() 方法来逐个比较。这也是为什么总说,覆写equals()时总是要覆写hashCode(),因为总是要保证,equals()相等的对象,hashcode值必须相等。

#########################################################################################

Java泛型

一个简单的例子说明泛型的作用:

早期的Object类型可以接收任意的对象类型,但是在实际的使用中,会有类型转换的问题:

public static void main(String[] args) {
        //测试一下泛型的经典案例
        ArrayList arrayList = new ArrayList();
        arrayList.add("helloWorld");
        arrayList.add("taiziyenezha");
        arrayList.add(88);//由于集合没有做任何限定,任何类型都可以给其中存放
        for (int i = 0; i < arrayList.size(); i++) {
            //需求:打印每个字符串的长度,就要把对象转成String类型
            String str = (String) arrayList.get(i);
            System.out.println(str.length());
        }
    }

这里 Integar 转为 String 出现了问题,为了在编译阶段就可以发现这样的问题,Java引入了泛型:

ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("helloWorld");
arrayList.add("taiziyenezha");
arrayList.add(88);// 在编译阶段,编译器就会报错

常见泛型参数类型

T:任意类型 type
E:集合中元素的类型 element
K:key-value形式中的 key
V:key-value形式中的 value

以上都是约定俗成的符号,你也可以不用这些字母表示,但是一般情况下还是建议使用相对应的字母。


泛型类,泛型方法,泛型接口

泛型类

一个例子:

public class GenericsClassDemo<T> {
    //t这个成员变量的类型为T,T的类型由外部指定
    private T t;
    //泛型构造方法形参t的类型也为T,T的类型由外部指定
    public GenericsClassDemo(T t) {
        this.t = t;
    }
    //泛型方法getT的返回值类型为T,T的类型由外部指定
    public T getT() {
        return t;
    }
}

泛型在定义的时候不具体,使用的时候才变得具体。即:在创建对象的时候确定泛型

例如,可以创建String类型的对象:
Generic<String> genericString = new Generic<String>("helloGenerics");
也可以创建Integer类型的对象:
Generic<Integer> genericInteger = new Generic<Integer>(666);

Note:在实例化时也可以不确定泛型的类型,比如使用 Generic<?> genericInteger = new Generic<>(666);,但这样就等于没有使用泛型,容易出现类型强转的问题。


泛型方法

一个例子:

/**
     *
     * @param t 传入泛型的参数
     * @param <T> 泛型的类型
     * @return T 返回值为T类型
     * 说明:
     *   1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
     *   2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
     *   3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
     */
    public <T> T genercMethod(T t){
        System.out.println(t.getClass());
        System.out.println(t);
        return t;
    }

同样是在调用方法时,才确定泛型的类型:

public static void main(String[] args) {
    GenericsClassDemo<String> genericString  = new GenericsClassDemo("helloGeneric"); //这里的泛型跟下面调用的泛型方法可以不一样。
    String str = genericString.genercMethod("hello");//传入的是String类型,返回的也是String类型
    Integer i = genericString.genercMethod(123);//传入的是Integer类型,返回的也是Integer类型
}

这里可以看出,泛型方法随着我们的传入参数类型不同,他得到的类型也不同。泛型方法能使方法独立于类而产生变化。


泛型接口
一个例子:

public interface GenericsInteface<T> {
    public abstract void add(T t); 
}

接口的泛型类型的确定既可以在定义类时发生:

public class GenericsImp implements GenericsInteface<String> {
    @Override
    public void add(String s) {
        System.out.println("设置了泛型为String类型");
    }
}

也可以在创建对象时再确定:

public class GenericsImp<T> implements GenericsInteface<T> {
    @Override
    public void add(T t) {
        System.out.println("没有设置类型");
    }
}

public class GenericsTest {
    public static void main(String[] args) {
        GenericsImp<Integer> gi = new GenericsImp<>();
        gi.add(66);
    }
}

泛型通配符

当不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。例如:

public static void main(String[] args) {
    ArrayList<Integer> list1 = new ArrayList<Integer>();
    test(list1);
    ArrayList<String> list2 = new ArrayList<String>();
    test(list2);
}
public static void test(ArrayList<?> coll){
}

这样的话,一个方法就可以接受不同类型的参数输入。

除了上述那种可以匹配任意类型的通配符,还有一种常用的通配符,可以设定泛型的上界或者下界

泛型的上界类型名称 <? extends 类 > 对象名称,只能接收该类型及其子类
泛型的下界类型名称 <? super 类 > 对象名称,只能接收该类型及其父类

比如:现已知Object类,Animal类,Dog类,Cat类,其中Animal是Dog,Cat的父类

//        ArrayList<? extends Animal> list = new ArrayList<Object>();//报错
        ArrayList<? extends Animal> list2 = new ArrayList<Animal>();
        ArrayList<? extends Animal> list3 = new ArrayList<Dog>();
        ArrayList<? extends Animal> list4 = new ArrayList<Cat>();

这里由于<? extends Animal>限定了只能为Animal或其子类,因此ArrayList<? extends Animal> list = new ArrayList<Object>()是错误的

        ArrayList<? super Animal> list5 = new ArrayList<Object>();
        ArrayList<? super Animal> list6 = new ArrayList<Animal>();
//        ArrayList<? super Animal> list7 = new ArrayList<Dog>();//报错
//        ArrayList<? super Animal> list8 = new ArrayList<Cat>();//报错

同样<? super Animal>限定了只能为Animal或其父类,所以对象类型为Dog或者Cat时会报错。

使用场景

大部分时候是将它作为方法中的形参来使用。例如 Fruit类 是 Apple类,Pear类,Orange类的父类。假设现在有Produce类:

class Produce<E> {
     public void produce(List<E> list) {
           for (E e : list) {
                //生产...
                System.out.println("批量生产...");
           }
     }
}

我们开始想用它生产梨子,那么有:

Producer<Pear> p = new Produce<>();
List<Pear> pears = new ArrayList<Pear>();
p.produce(pears);

但如果现在又想生产苹果了,此时编译并不能通过。因为初始化时 Producer<Pear> p = new Produce<>();已经确认了输入类型为 Pear,此时不能再兼容 Apple,除非再实例化一个新的对象。这种情况就可以使用List<? extends E> list了:

class Producer<E> {
     public void produce(List<? extends E> list) {
           for (E e : list) { //利用<? extends E>读取的特性
                //生产...
           }
         System.out.println("批量生产完成...");
     }
}

//苹果和梨子都可以编译通过
Producer<Fruit> p = new Produce<>();
List<Apple> apples = new ArrayList<>();
p.produce(apples);
List<Pear> pears = new ArrayList<>();
p.produce(pears);

#########################################################################################

JVM,JDK和JRE

  • JDK(Java Development Kit),JAVA开发工具包
  • JRE(Java Runtime Environment ),Java的运行环境
  • JVM(Java Virtual Machine),Java虚拟机

虚拟机

虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

一个java代码的编译和运行方式:

public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("HelloWorld");
	}
}

编译:

zhangjg@linux:/deve/workspace/HelloJava/src$ javac HelloWorld.java 
zhangjg@linux:/deve/workspace/HelloJava/src$ ls
HelloWorld.class  HelloWorld.java

运行:

zhangjg@linux:/deve/workspace/HelloJava/src$ java -classpath . HelloWorld 
HelloWorld

从上面的过程可以看到, 我们在运行Java版的HelloWorld程序的时候, 敲入的命令并不是 ./HelloWorld.class 。 因为class文件并不是可以直接被操作系统识别的二进制可执行文件 。 我们敲入的是java这个命令。 这个命令说明, 我们首先启动的是一个叫做java的程序, 这个java程序在运行起来之后就是一个JVM进程实例

java命令首先启动虚拟机进程,虚拟机进程成功启动后,读取参数“HelloWorld”,把他作为初始类加载到内存,对这个类进行初始化和动态链接(加载的ddl就需要在JRE里面找,因此JVM是依赖于JRE的),然后从这个类的main方法开始执行。也就是说我们的.class文件不是直接被系统加载后直接在cpu上执行的,而是被一个叫做虚拟机的进程托管的。首先必须虚拟机进程启动就绪,然后由虚拟机中的类加载器加载必要的class文件,包括jdk中的基础类(如String和Object等),然后由虚拟机进程解释class字节码指令,把这些字节码指令翻译成本机cpu能够识别的指令,才能在cpu上运行。

从这个层面上来看,在执行一个所谓的java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程,而不是我们写的一个个的class文件。这个叫做虚拟机的进程处理一些底层的操作,比如内存的分配和释放等等。我们编写的class文件只是虚拟机进程执行时需要的“原料”。这些“原料”在运行时被加载到虚拟机中,被虚拟机解释执行,以控制虚拟机实现我们java代码中所定义的一些相对高层的操作,比如创建一个文件等,可以将class文件中的信息看做对虚拟机的控制信息,也就是一种虚拟指令。

JVM和JRE

JVM不能单独搞定class的执行,解释class的时候JVM需要调用解释所需要的类库lib。JRE目录里面有两个文件夹bin和lib,在这里可以认为bin里的就是JVM,lib中则是JVM工作所需要的类库,而JVM和 lib和起来就称为JRE。

#########################################################################################


Reference:

  1. 深入理解Java虚拟机到底是什么
  2. hash算法原理详解
  3. Java 泛型详解
  4. Java中线程状态+线程安全问题+synchronized的用法详解
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值