目录
类
抽象类和普通类的区别
先来看看什么是 抽象类:
- 抽象类:含有抽象方法的类叫作抽象类
- 抽象方法:必须用"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...
重写
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 要求:
- 子类方法参数列表与父类方法的参数列表必须完全相同。
- 子类方法的返回类型必须是父类方法返回类型或其子类型。
- 子类方法的访问权限必须大于等于父类方法。
- 子类方法抛出的异常类型必须是父类抛出异常类型或其子类型。
- 父类被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。
- 首先看this.show(O),发现B里没有以C为输入的show方法
- 再看super.show(O),B的父类是A,A里也没有以C为输入的show方法
- 因此看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
- private:
可修饰数据成员,构造方法,方法成员,不能修饰类(此处指外部类)。被private修饰的成员,只能在定义它们的类内使用,其它类不能调用 - protected:
可修饰数据成员,构造方法,方法成员,不能修饰类(此处指外部类)。被protected修饰的成员,能在定义它的类中,同package的类中被调用。如果有不同package的类想调用它,那么这个类必须是定义它的类的子类。 - 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修饰类:
- 只允许修饰内部类(写在类内的类),只能访问静态的成员变量和方法,不能访问非静态的方法和属性,但是普通内部类可以访问任意外部类的成员变量和方法
- 静态内部类可以声明普通成员变量和方法,而普通内部类不能声明静态成员变量和方法
- 静态内部类使用场景一般是当外部类需要使用内部类,而内部类无需外部类资源,并且内部类可以单独创建时
- 静态内部类可以单独初始化:
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关键字
- 当成员变量和局部变量重名时,在方法中使用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 引用。
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()
表示对父类对象的引用,主要有以下两种使用场景:
- 访问父类的构造函数:可以使用
super()
函数访问父类的构造函数,从而委托父类完成⼀些初始化的工作。应该注意到,子类⼀定会调用父类的构造函数来完成初始化工作,⼀般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用super()
函数。 - 访问父类的成员方法和成员属性:如果子类重写了父类的某个方法,可以通过使⽤
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的情况是:
![](https://i-blog.csdnimg.cn/blog_migrate/5ce9695d0f0244ddbb027ff68c671e11.png)
builder.append("4")
之后:
![](https://i-blog.csdnimg.cn/blog_migrate/b8dc77866006e87933ddf50b64eddbc5.png)
sb
本身没有变,但是builder.append("4")
把它指向的内容改变了
再看例4,传递参数时是一样的:
![](https://i-blog.csdnimg.cn/blog_migrate/5ce9695d0f0244ddbb027ff68c671e11.png)
但由于builder
是重新new了一个StringBuilder
,因此它开辟了一块新空间:
![](https://i-blog.csdnimg.cn/blog_migrate/bf98cc9d67bf938362385ad07ff83e34.png)
所以builder
做的任何操作都不会影响到sb
。
以上就解释了,为什么都是值传递,但有的实参会受影响,而有的不会。
#########################################################################################
哈希
哈希(也称“散列”)就是把任意长度的输入,通过散列算法,变换成固定长度的输出。总体而言,哈希函数用于,将消息或数据压缩,生成数据摘要,最终使数据量变小,并拥有固定格式。
特点:
- 相同的输入一定得到相同的输出;
- 不同的输入大概率得到不同的输出。
作用:
- 哈希使数据的存储与查询速度更快。
- 由于无法根据输出得到输入,哈希能对信息进行加密处理,使得数据传播更为安全。
主要步骤:
- 根据哈希函数取得哈希值
- 处理哈希冲突
各种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,对于序列中的每一个元素,计算其哈希值作为在哈希表中的索引位置,而这个元素就放入该位置。那么哈希表如下:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
20 | 30 | 50 | 70 | 80 |
这样,当进来一个新的元素 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: