Java基础

程序的执行顺序

class Print {
	Print() {
		System.out.println("haha");
	}
}

class A {
	public A() {// 构造函数
		System.out.println("class A");
	}

	{ // 代码块
		System.out.println("I'm A class");
	}
	static Print test1 = new Print();
	static { // 静态代码块
		System.out.println("class A static");
	}
}

public class B extends A {
	public B() {// 构造函数
		System.out.println("class B");
	}

	{ // 代码块
		System.out.println("I'm B class");
	}
	static {
		System.out.println("class B static");
	} // 静态代码块

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

执行结果:

haha
class A static
class B static
I'm A class
class A
I'm B class
class B

I'm A class
class A
I'm B class
class B

结论
如果类还没有被加载:
(1)先执行父类的静态代码块和静态属性初始化,并且静态代码块和静态属性的执行顺序只与代码中出现的顺序有关
(2)执行子类的静态代码块和静态属性初始化
(3)执行父类的非静态代码块和属性初始化,并且非静态代码块和非静态属性的执行顺序只与代码中出现的顺序有关
(4)执行父类构造函数
(5)执行子类的非静态代码块和属性初始化
(6)执行子类的构造函数
如果类已经被加载:
(1)则静态代码块和静态属性不再重复执行

静态变量存储在方法区,属于类所有,实例变量存储在堆中,其引用存储在当前线程栈
参考网址

Java中基本类型和包装类

基本数据类型

Java 为每个原始类型提供了包装类型:

  • 原始类型: boolean,char,byte,short,int,long,float,double
  • 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double

包装类

包装类为不可变对象,任何修改都会创建一个新的对象
Java中的基本类型不是面向对象的,它们只是纯粹的数据,除了数值本身的信息之外,基本类型数据不带有其他信息或者可操作方法。这在实际使用中存在很多不足,为了解决这个不足,对每个基本类型都对应了一个引用的类型,称为装箱基本类型。

1 装箱、拆箱
装箱:根据数据创建相应的包装对象。

Integer i = new Integer(3);
Integer j = 4;//自动装箱
  • JDK1.5 为Integer 增加了一个全新的方法:public static Integer valueOf(int i)
    在自动装箱过程时,编译器调用的是static Integer valueOf(int i)这个方法。
    于是Integer j = 4; ==> Integer j = Integer.valueOf(4);

  • 此方法与new Integer(i)的不同处在于: 方法一调用类方法返回一个表示 指定的 int 值的 Integer
    实例。方法二产生一个新的Integer 对象。

拆箱:将包装类型转换为基本数据类型。

int i1=i;//自动拆箱
int j1=j.intValue();

2 缓冲机制
如果不需要新的 Integer 实例,则通常应优先使用 valueOf 方法,而不是构造方法 Integer(int),因为该方法有可能通过缓存经常请求的值而显著提高空间和时间性能 。
查看Integer的valueOf方法的:

    public static Integer valueOf(int i) {
        //-128=<i<=127的时候,就直接在缓存中取出
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        ///否则就在堆内存中创建
        return new Integer(i);
    }    
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];//cache 缓存是一个存放Integer类型的数组

        static {//初始化,最大值可以配置
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];//初始化数组
            int j = low;
            //缓存区间数据
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

基本类型大小基本类型缺省默认值包装类型包装类型缓冲机制
boolean/falseBoolean全部缓存
char16bit'\u0000'Character<=127缓存
byte8bit0Byte全部缓存
short16bit0Short-128~127缓存
int32bit0Integer-128~127缓存
long64bit0Long-128~127缓存
folat32bit0.0Float无缓存
double64bit0.0Double无缓存
包装类型缺省默认值为null

String StringBuffer StringBuilder

执行速度是否线程安全
String最慢
StringBuffer其次
StringBuilder最快
  • 执行速度:String最慢的原因,String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全,而StringBuffer则每次都需要判断锁,效率相对更低。
  • 线程安全:String是字符串常量,故为线程安全。而如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。
  • String:适用于少量的字符串操作的情况
    StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
    StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

String

  • 字符串常量,即不可变对象。不可变对象是指一个对象的状态在对象被创建之后就不再变化。不可改变的意思就是说:不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。
    String 不可变是因为在 JDK 中 String 类被声明为一个 final 类,且类内部的 value 字节数组也是 final 的,private final char value[];只有当字符串是不可变时字符串池才有可能实现,字符串池的实现可以在运行时节约很多 heap 空间,因为不同的字符串变量都指向池中的同一个字符串;如果字符串是可变的则会引起很严重的安全问题,譬如数据库的用户名密码都是以字符串的形式传入来获得数据库的连接,或者在 socket 编程中主机名和端口都是以字符串的形式传入,因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子改变字符串指向的对象的值造成安全漏洞;因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享,这样便不用因为线程安全问题而使用同步,字符串自己便是线程安全的;
    因为字符串是不可变的所以在它创建的时候 hashcode 就被缓存了,不变性也保证了 hash 码的唯一性,不需要重新计算,这就使得字符串很适合作为 Map 的键,字符串的处理速度要快过其它的键对象,这就是 HashMap 中的键往往都使用字符串的原因。

  • 定义String的方法:
    1 String str1 = “hello”;
    2 String str2 = new String(“hello”);
    第一种方法:引用str1被存放在栈区,字符串常量"hello"被存放在常量池,引用str1指向了常量池中的"hello"(str1中的存放了常量池中"hello"的地址)。
    第二种方法:引用str2被存放在栈区,同时在堆区开辟一块内存用于存放一个新的String类型对象。(同上,str2指向了堆区新开辟的String类型的对象)

  • String类的intern()方法:
    判断这个常量是否存在于常量池。
      如果存在
       判断存在内容是引用还是常量,
        如果是引用,
         返回引用地址指向堆空间对象,
        如果是常量,
         直接返回常量池常量
      如果不存在,
       将当前对象引用复制到常量池,并且返回的是当前对象的引用

  • 在做字符串拼接操作,也就是字符串相"+"的时候,得出的结果是存在在常量池或者堆里面,这个是根据情况不同不一定的:表达式右边是纯字符串常量,那么存放在栈里面。表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面。

    String str1 = “aaa”;
    String str2 = “bbb”;
    String str3 = “aaabbb”;
    String str4 = str1 + str2;
    String str5 = “aaa” + “bbb”;
    System.out.println(str3 == str4); // false
    System.out.println(str3 == str4.intern()); // true
    System.out.println(str3 == str5);// true

StringBuffer

  • 字符串变量,StringBuffer可变是因为StringBuffer的append方法的底层实现就是创建一个新的目标对象,然后将各个字符引用串接起来,这就是一个新的对象了,本质上就是栈多了一个变量,而堆上面并没有变化(为什么是在栈上呢?因为触发到的arraycopy方法是native的,也就是其生成的变量会放到本地方法栈上面去)。 这样将线程私有的对象打散分配在栈上,可以在函数调用结束后自行销毁对象,不需要垃圾回收器的介入,有效避免垃圾回收带来的负面影响,栈上分配速度快,提高系统性能
  • StringBuffer和StringBuilder都继承了AbstractStringBuilder
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);//调用下面的append()
        return this;
    }
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

StringBuilder

和StringBuffer一样继承AbstractStringBuilder,但是是非线程安全的

== equals() hashCode()

==

  • 基本数据类型(byte short char int long float double boolean):比较的是他们的值
  • 引用数据类型:比较的是在内存中存放的地址(确切的说是堆内存的地址),除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。因为每new一次,都会重新开辟堆内存空间。

equals()

引用类型:默认情况下,比较的是内存地址值。在一些类库当中这个方法被重写了,如String、Integer、Date。在这些类当中equals有其自身的实现(一般都是用来比较对象的成员变量值是否相同),而不再是比较类在堆内存中的存放地址了。

hashCode

Object若不重写hashCode()的话,hashCode()直接返回对象的内存地址.

  • 对象的比较的原则如下:
    1 两个obj,如果equals()相等,hashCode()一定相等。
    2 两个obj,如果hashCode()相等,equals()不一定相等(Hash散列值有冲突的情况,虽然概率很低)。
  • 在集合中,判断两个对象是否相等的规则是:
    如果hashCode()相等,则查看equals()是否相等,相等则为同一对象。
  • 改写equals()时总是要改写hashCode():
    则会违反约定的第二条:相等的对象必须具有相等的散列码(hashCode)。同时在HashMap中,如果要比较key是否相等,要同时使用这两个函数!因为自定义的类的hashcode()方法继承于Object类,其hashcode码为默认的内存地址,这样即便有相同含义的两个对象,比较也是不相等的。HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等的。若equals()不相等则认为他们不相等。如果只重写hashcode()不重写equals()方法,当比较equals()时只是看他们是否为同一对象(即进行内存地址的比较),所以必定要两个方法一起重写。HashMap用来判断key是否相等的方法,其实是调用了HashSet判断加入元素 是否相等。重载hashCode()是为了对同一个key,能得到相同的Hash Code,这样HashMap就可以定位到我们指定的key上。重载equals()是为了向HashMap表明当前对象和key上所保存的对象是相等的,这样我们才真正地获得了这个key所对应的这个键值对。

例:HashSet中使用equals方法

public class Student {
    private int id;
    private String name;
    public Student(int id, String name) {
        this.name = name;
        this.id = id;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
	@Override
	public boolean equals(Object obj) {
	    if (obj == null) return false;
	    if (!(obj instanceof Student))
	        return false;
	    if (obj == this)
	        return true;
	    return this.getId() == ((Student) obj).getId();
	}
}
public class HashcodeEquals {
    public static void main(String[] args) {
        Student alex1 = new Student(1, "Alex");
        Student alex2 = new Student(1, "Alex");
        HashSet < Student > students = new HashSet < Student > ();
        students.add(alex1);
        students.add(alex2);
        System.out.println("HashSet size = " + students.size());
        System.out.println("HashSet contains Alex = " + students.contains(new Student(1, "Alex")));
    }
}

输出

HashSet size = 2 
HashSet contains Alex = false

上述例子中,尽管我们已经重载了equals方法,理论上 alex1与 alex1应该是同一个对象,而HashSet是无法存储重复对象,但为什么JVM会认为他们是不同的对象。

这涉及到了HashSet的内部存储结构,在JDK中,HashSet将其内部元素存储在内存桶中。每个内存桶都关联一个特定的哈希值。在调用student.add(alex1)时,Java将alex1存储在一个内存桶中,并将其与alex1.hashCode()的值关联。后续,一旦插入相同哈希值的对象,那么插入对象将替换原有对象alex1。但是,由于alex2与alex1哈希值不同,因此被JVM视为完全不同的对象,将其存储在另外一个单独的内存桶中。

因此,我们按照业务场景重载hashCode方法,确保拥有相同ID的Student实例存储在同一个内存桶中,代码如下:

@Override
public int hashCode() {
    return id;
}

关键字

final

(1) 修饰类

  • 该类不能被继承
  • 所有的成员方法都会隐式的定义为final方法。

(2)修饰方法

  • 把方法锁定,以防止继承类对其进行更改。
  • 若父类中final方法的访问权限为private,将导致子类中不能直接继承该方法,因此,此时可以在子类中定义相同方法名的函数,此时不会与重写final的矛盾,而是在子类中重新地定义了新方法。

(3)修饰变量

  • final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;
  • final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的
  • 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));
    }
}

输出:

true
false

当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。由于变量b被final修饰,因此会被当做编译器常量,所以在使用到b的地方会直接将变量b 替换为它的 值。而对于变量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));
 
    }
     
    public static String getHello() {
        return "hello";
    }
}

输出:

false

Synchronized和lock

synchronized

  • Java的关键字,是内置的语言实现
  • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生
  • synchronized不可以让等待锁的线程响应中断
  • 不能知道有没有成功获取锁
  • 当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码
  • JDK1.5以后引入了自旋锁、锁粗化、轻量级锁,偏向锁来有优化关键字的性能。

Lock

  • Lock是一个接口
  • 而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  • Lock可以让等待锁的线程响应中断
  • 通过Lock可以知道有没有成功获取锁

volatile

  • volatile关键字是用来保证有序性和可见性的。
  • 这跟Java内存模型有关。比如我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,CPU也会做重排序的,这样的重排序是为了减少流水线的阻塞的,引起流水阻塞,比如数据相关性,提高CPU的执行效率。
  • happens-before规则,其中有条就是volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;有序性实现的是通过插入内存屏障来保证的。可见性:首先Java内存模型分为,主内存,工作内存。比如线程A从主内存把变量从主内存读到了自己的工作内存中,做了加1的操作,但是此时没有将i的最新值刷新会主内存中,线程B此时读到的还是i的旧值。加了volatile关键字的代码生成的汇编代码发现,会多出一个lock前缀指令。Lock指令对Intel平台的CPU,早期是锁总线,这样代价太高了,后面提出了缓存一致性协议,MESI,来保证了多核之间数据不一致性问题。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值