花写完这篇文章,我感触很深,以前学Java认为会用就行了,而不会刨根问底,导致很多知识都停留在表面,思考得不透彻。经过这次较全面的整理,可以知道以前知道的还是太少了。学习就要一步一个脚印,才会扎实。
Java程序的执行过程,必须经过先编译,后解释两个步骤。Java代码使用javac编译生成.class文件(字节码文件),交给JVM进行解析执行。
面向对象和基于对象都实现了“封装”的概念,但面向对象实现了“继承”和“多态”,而基于对象没有。JavaScript是基于对象的,它使用已封装好的对象,调用对象的方法,但它无法让开发者派生新的类,只能使用现有的方法和属性。
动态绑定技术(dynamic binding),执行期间判断所引用对象的实际类型,根据实际类型调用对应的方法
以下修饰符只能用于修饰成员变量,不能修饰局部变量;private与protected不能用来修饰类(只有public、abstract或final能用来修饰类)。
相同:都不能实例化、都可定义抽象方法、静态方法、默认方法(Java8开始)
不同:
对象一旦被创建,状态就不能再改变,如 String、Integer及其它包装类。
方法重载:方法名相同,但形参列表不同。
方法重写:子类包含与父类同名的方法,有相同返回类型。
区别:重载主要发生在同一个类的多个同名方法之间,而重写发生在子类和父类的同名方法之间。
工厂模式是最常用的实例化对象模式,是用工厂方法代替new操作的一种模式。
作用:如果有多处需要生成A的对象,那么你需要写很多A a=new A(),需要修改时,就会很麻烦!但是如果用工厂模式,只需要修改工厂代码。
public interface Shape {
void draw();
}
public class Rectangle implements Shape {
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}
public class Square implements Shape {
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}
public class ShapeFactory {
//使用 getShape 方法获取形状类型的对象
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
} else if(shapeType.equalsIgnoreCase("SQUARE")){
return new Square();
}
return null;
}
}
提供更好的封装,不允许同一包类的其他类访问
可直接访问外部类私有数据
匿名内部类适用于创建那些仅需要使用一次的类
当JVM执行可恢复对象的finalize()方法时,可变成可达状态,但什么时候调用该方法并不确定。
调用System类的gc()静态方法:System.gc()(只通知GC进行回收,但什么时候回收不确定)
调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()
在Java中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型。
我们知道不可达的对象就可以被回收,问题就转化为:如何判断对象是可达还是不可达?
可达性算法(引用链法)
从GC roots对象(根对象)开始向下搜寻,如果从根对象开始无法引用到该对象,则该对象就是不可达的。
以下三类对象在jvm中作为GC roots:虚拟机栈(JVM stack)中引用的对象、方法区中类静态属性引用的对象、本地方法栈(Native Stack)引用的对象
==:用于比较两个变量是否相等(基本数据类型)或比较两个引用变量是否指向同一个对象
equals():是Object类的方法,同样用于比较两个引用变量是否指向同一个对象(其中String已经重写了该方法,比较的是两字符串的值)
hashCode()是Object类的一个方法,返回一个哈希值。如果两个对象根据equal()方法比较相等,那么它们调用hashCode()方法返回的哈希值相同。但如果不相等,返回的哈希值不一定不等(冲突情况下还是会相等的——两个不相等的对象有可能有相同的哈希值)。
false,因为有些浮点数不能完全精确的表示出来。
不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。
对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋值给short型。而short s1 = 1; s1 += 1;可以正确编译,因为s1+= 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
Integer是int的包装类,int是基本类型,i=5;直接在栈内存分配空间;而Integer是类,Integer i = new Integr(5);,对象的数据是存在堆内存中,而i(引用变量)是在栈内存中。
(JVM管理的是堆内存,在堆内存中分配空间所需的时间远大于从栈中分配存储空间,所以JAVA速度比C 慢。)
String实现了常量池!!
第二种方法直接调用构造函数来创建字符串,如果所创建的字符串在字符串常量池中不存在则调用构造函数创建全新的字符串,如果所创建的字符串在字符串常量池中已有则再拷贝一份到 Java 堆中。
(1)栈内存:用于保存基本数据类型的值、引用变量、局部变量
(2)堆内存:用于保存new产生的对象,数组(对象只包含成员变量,不包括成员方法。同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法)
(3)方法区:方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量,被所有线程共享
常量池:存在于方法区中,存放字符串常量和基本类型(指包装类)变量。(含有[-128,127]的值供实现的常量池的基本类型引用)
package Controller;
public class test {
public static void main(String[] args) {
String aa="ab";//放在常量池中
String bb="ab";//在常量池中查找
String a=new String("ab");
String b=new String("ab");//a,b分别位于堆中不同的内存空间
System.out.println(a.equals(b));//true
System.out.println(aa==bb);//true
System.out.println(a==b);//false
System.out.println(a==bb);//false
System.out.println(a.equals(bb));//true
int c=128; //放在栈中,指向128的内存地址
float d=128.0f; //放在栈中,指向128的内存地址
double e=128.0; //放在栈中,指向128的内存地址 ,即栈内数据是共享的
Integer cc=new Integer(128);//cc放在栈中,指向堆中的对象
System.out.println(c==d);//true
System.out.println(c==e);//true
System.out.println(c==cc);//true,/这里实际上是:c == cc.intValue()(自动拆箱)
System.out.println(cc.equals(c));//true
System.out.println(cc.equals(e));//false
Integer i1=new Integer(1);
Integer i2=new Integer(1); //i1,i2分别位于堆中不同的内存空间
Integer i12=new Integer(2);
System.out.println(i1==i2);//输出false
System.out.println(i12==i1+i2);//输出true,Java的数学运算都是在栈中进行的,Java会自动对i1、i2进行拆箱操作转化成整型
Integer i3=1;
Integer i4=1; //i3,i4指向常量池中同一个内存空间 ,Integer在常量池的范围为-128~127
System.out.println(i3==i4);//输出true
Integer i5=129;
Integer i6=129; //i5,i6指向堆中不同的内存空间
System.out.println(i5==i6);//输出false
}
}
i++:在程序执行完后进行自增;++i:在程序开始执行前进行自增
public static void main(String[] aegs){
int i=1;
System.out.println(i++); //1
System.out.println(++i); //3
int j=1;
System.out.println(j+++j++); //3
System.out.println(j); //3
System.out.println(j+++ ++j); //8
System.out.println(j); //5
System.out.println(j+++j+++j++);//18
System.out.println(j); //8
}
String是字符串常量,默认final修饰,是一个对象,而不是基本数据类型;StringBuffer字符串变量(线程安全);StringBuilder 字符串变量(线程不安全)。
(1)String和StringBuffer
String和StringBuffer主要区别是性能。String是不可变对象,每次对String类型进行操作都等同于产生了一个新的String对象,然后指向新的String对象;StringBuffer是对对象本身操作,而不是产生新的对象。
(2)StringBuffer和StringBuilder
StringBuffer是线程安全的可变字符串,其内部实现是可变数组。StringBuilder是Java 5.0新增的,其功能和StringBuffer类似,但是非线程安全。因此,在没有多线程问题的前提下,使用StringBuilder会取得更好的性能。
公共静态不可变(public static final )变量就是编译期常量,这里的 public 可选的。它在编译期就可以确定,调用编译期常量不会初始化类。
package testPage;
class InitalizedClass {
static {
System.out.println("You have initalized InitalizedClass!");
}
public static int inititalize_varible = 1;
}
public class TestInitializeClass {
public static void main(String[] args) {
System.out.println(InitalizedClass.inititalize_varible);
}
/**
* 输出结果为:
* You have initalized InitalizedClass!
* 1
*/
}
package testPage;
class InitalizedClass {
static {
System.out.println("You have initalized InitalizedClass!");
}
public final static int INITIALIZED_VARIBLE = 1;//编译器常量
}
public class TestInitializeClass {
public static void main(String[] args) {
System.out.println(InitalizedClass.INITIALIZED_VARIBLE);
}
/**
* 输出结果为:
* 1
*/
}
继承和组合都是实现类复用的重要手段,不同的是继承会破坏封装(重写改变方法实现),而组合能提供更好的封装性。继承表达的是“is a”的关系,而组合表达的是“has a”的关系。
遵循这样一个原则:能使用组合的时候尽量不要使用继承。除非两个类之间是“is-a”的关系,否则不要轻易地使用继承,因为过多地使用继承会破坏代码的可维护性,当父类被修改的时候,会影响到所有继承自它的子类,从而增加程序的维护难度与成本。
如两个方法 void a();和 int a(),如果这样调用int result=a();,系统是可以识别调用了返回值类型为int的方法,但java里允许调用一个有返回值的方法的时候不必将返回值赋给变量,这样JVM就不知道你调用的是有返回值的还是没返回值的。
public class Father {
public String name="父亲属性";
public void method() {
System.out.println("父类方法,对象类型:" + this.getClass());
}
public void method1(){
System.out.println("父类方法1,对象类型:" + this.getClass());
}
}
public class Son extends Father{
public String name="儿子属性";
public void method() {
System.out.println("子类方法,对象类型:" + this.getClass());
}
public static void main(String[] args) {
Father s1 = new Son();//向上转型
System.out.println("调用的成员:"+s1.name);
s1.method();
s1.method1();
Son s2=(Son) s1;//向下转型 ,而Son s2=(Son) new Farther();会抛出java.lang.ClassCastException异常
System.out.println("调用的成员:"+s2.name);
s2.method();
s2.method1();//this总是指向调用该方法的对象
}
}
结果
调用的成员:父亲属性
子类方法,对象类型:class Controller.Son
父类方法1,对象类型:class Controller.Son
调用的成员:儿子属性
子类方法,对象类型:class Controller.Son
父类方法1,对象类型:class Controller.Son
List a=new ArrayList();
a.add(6); //java支持自动装箱,实际放进去的是int的包装类Integer
不能,但Java支持基本类型的自动装箱。
38、Java集合框架的基础接口有哪些
java集合类主要由两个根接口派生而出:Collection和Map,Collection下还有以下三个接口
Iterator接口提供遍历任何Collection的接口,Iterrator对象也被称为迭代器。Iterator是安全的,可通过Iterator的remove()方法删除元素,但不允许在迭代过程中其他线程修改集合结构,它采用的是快速失败(fail-fast)机制,一旦检测到其他线程修改,就会触发ConcurrentModificationException异常。
fail-fast机制在遍历一个集合时,当集合结构被修改,会抛出Concurrent Modification Exception
Map premiumPhone = new HashMap();
premiumPhone.put("Apple", "iPhone");
premiumPhone.put("HTC", "HTC one");
premiumPhone.put("Samsung", "S5");
Iterator iterator = premiumPhone.keySet().iterator();
while (iterator.hasNext()) {
System.out.println(premiumPhone.get(iterator.next()));
premiumPhone.put("Sony", "Xperia Z");
}
如果改成如下
premiumPhone.put("HTC", "Xperia Z");
则不会抛出ConcurrentModification 异常。因为只修改了值,没有改变集合的结构。
Java.util包中的所有集合类都被设计为fail-fast的,而java.util.concurrent中的并发集合类都为fail-safe的。
fail-safe原理是:任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException异常。
fail-safe机制有两个问题:引入额外的空间开销、无法保证读取的数据是原始数据
HashSet是基于HashMap实现的,集合元素实际由HashMap的Key来保存,HashSet访问集合元素时根据元素的hashCode值来快速定位
HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。因此,放进HashSet的对象必须重写equals()和hashCode()方法,并且保证两对象equals相等时,它们返回的哈希值也相等。以维护常规原则——相等的对象必须具有相等的哈希值。
将对象放入到HashSet中时,根据对象的hashcode值快速定位,找到该对象在数组储存的位置,接着找到该位置保存的对象,并调用equals方法比较这两个对象是否相等,如果相等则不添加,如果不等,则添加到该数组索引对应的链表中。
TreeSet是SortedSet接口的实现类,是基于TreeMap(红黑树)实现的,元素处于排序状态,支持两种排序方式:
public class M {
public int age;
public M(int age) {
this.age=age;
}
public String toString(){
return "M[age:"+age+"]";
}
}
public class Test {
public static void main(String[] args) {
TreeSet t=new TreeSet(new Comparator() {
@Override
public int compare(M o1, M o2) {
return o1.age>o2.age ? -1 : o1.age
注:HashSet和TreeSet集合中推荐只放入不可变对象。
Comparable可以认为是一个内比较器,实现了Comparable接口的类可以和自己比较,依赖compareTo方法,一般实现自然排序。(a.compareTo(b)=0,则a=b;a.compareTo(b)>0,则a>b;a.compareTo(b)<0,则a
Comparator可以认为是一个外比较器,实现了Comparator接口的类通过compare方法定制自己想要的比较方式,可实现定制排序。(compare(a,b)=0,则a=b;compare(a,b)>0,则a>b;compare(a,b)<0,则a
47、历遍一个List集合有哪些方法
48、ArrayList与Vector的异同
ArrayList与Vector都是List的实现类,一般推荐使用ArrayList。
尽管ArrayList明显是更好的选择,但也有些时候Array比较好用。
两者都实现的是List接口,不同之处在于:
此外,LinkedList实现了Deque接口,是双向链表的,可被用作栈(stack),队列(queue)或双向队列(deque)。注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是使用Collections工具类:List list = Collections.synchronizedList(new LinkedList(…));
使用Iterator的remove()方法
前者:随机访问时性能更好
后者:执行删除、插入操作时性能更好,而且迭代操作的性能也更好
我们知道数组寻址容易,插入和删除困难;链表寻址困难,插入和删除容易。而哈希表综合了两者的优点,既满足了数据的查找方便,同时不占用太多的内容空间。其中一种基于链地址法的哈希表如下:
HashMap用于储存键值对,它内部是通过一个数组实现的,只是这个数组比较特殊,数组里存储的元素是一个Entry对象,这个Entry对象包含了key、value以及一个Entry对象next(用于指向下一个Entry对象)。HashMap是基于hashing实现的:
(1)当两个对象的hashcode相同会发生什么
定位到同一个bucket,发生冲突
(2)如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办
HashMap默认的负载因子是0.75,会创建一个是原数组大小两倍的数组,并将原来的对象放入新的数组中,这个过程叫rehashing。
(3)我们可以使用自定义的对象作为键吗?
所定义的对象必须重写equals方法和hashcode方法,并且满足相等的对象必须具有相等的哈希值,为了防止放进HashMap的对象被修改(修改后,通过get方法可能会找不回到),可将对象定义成不可变对象。
ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素
都是Map接口的典型实现类,使用Collections工具类可以将HashMap变成线程安全,或使用对应的并发集合类CocurrentHashMap
相同点: Hashtable 和 ConcurrentHashMap都是线程安全的,可以在多线程环境中运行; key跟value都不能是null
区别: 两者主要是性能上的差异,Hashtable的所有操作都会锁住整个hash表,虽然能够保证线程安全,但是性能较差; ConcurrentHashMap锁的方式是对hash表局部锁定,不影响其他线程对hash表其他地方的访问,默认支持16个线程并发写入。并且ConcurrentHashMap的迭代器采用了fail-safe机制,因此不会抛出ConcurrentModificationException异常。
试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。
ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。
TreeMap是SortedMap接口的实现类,它就是一个红黑树数据结构,TreeMap保证所有的key-value对处于有序状态。跟TreeSet一样支持两种排序:
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。
并发集合类解决了传统集合类线程不安全的问题,提供大量支持高效并发访问的集合接口,这些线程安全的集合类可分为两类:
(1)以Concurrent开头的集合类可以支持多个线程并发写入访问,并且所有操作不会锁住整个集合,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue和ConcurrentLinkedDeque.
(2)以CopyOnWrite开头的集合类,如CopyOnWriteArrayList、CopyOnWriteArraySet.。CopyOnWriteArraySet.的底层封装了CopyOnWriteArrayList,所以两者的实现机制是完全一样的。它们采用复制底层数组的方式来实现写操作。
BlockingQueue(处于conurrent包下)——阻塞队列,用于实现多个线程间数据的共享,比如经典的“”生产者和消费者“模型”,BlockingQueue解决了生产者线程和消费者线程数据处理速度不匹配的问题。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)
Java提供了多个其实现类:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue和DelayQueue。
下面两幅图演示了BlockingQueue的两个常见阻塞场景:
Collections是操作集合的工具类,可提供大量方法对集合元素进行排序、查询和修改等操作,还提供将集合对象设置为不可变、对集合对象实现同步控制等方法。
大写的O描述的是一个算法的性。
poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
对象数组:Arrays.sort()方法
对象列表:Collection.sort()方法
在作为参数传递之前,使用Collections.unmodifiableCollection(Collection c)方法创建一个只读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。
可以使用Collections.synchronizedCollection(Collection c)根据指定集合来获取一个synchronized(线程安全的)集合。
线程是系统运行调度的最小单位,是进程的子集,同一进程中的多个线程之间可以并发执行。不同的进程使用不同的内存空间,而同一进程的所有的线程共享该进程的内存空间。
实现Runnable接口比继承Thread类更优:(缺点是:访问当前线程必须使用Thread.currentThread()方法,而继承Thread类直接使用this即可访问)
Runnable与Callable的主要区别:
FutureTask表示一个异步运算的任务,可以将一个任务交给后台线程执行,并通过get方法返回执行结果。可以与Callable接口配合使用,方便获取多线程运行的结果。
新建、就绪、运行、阻塞和死亡
线程创建后,不会立即执行线程执行体,调用start()方法后处于就绪状态,当获得CPU开始执行run()方法,处于运行状态,当另一个线程抢占了CPU后,处于阻塞状态然后在合适的时候重新进入就绪状态,知道run()方法运行完进入死亡。
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法;而直接调用run()方法,只会是在原来的线程中调用,没有启动新的线程。
CPU控制权由一个正运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
CountDownLatch是一次性的,而CyclicBarrier能够重复使用。
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for(int i=0;i<5;i++){
new Thread(new readNum(i,countDownLatch)).start();
}
countDownLatch.await();
System.out.println("线程执行结束。。。。");
}
static class readNum implements Runnable{
private int id;
private CountDownLatch latch;
public readNum(int id,CountDownLatch latch){
this.id = id;
this.latch = latch;
}
@Override
public void run() {
synchronized (this){
System.out.println("id:"+id);
System.out.println("线程组任务"+id+"结束,其他任务继续");
latch.countDown();
}
}
}
}
运行结果:
id:0
id:2
id:3
线程组任务3结束,其他任务继续
id:4
线程组任务4结束,其他任务继续
id:1
线程组任务1结束,其他任务继续
线程组任务2结束,其他任务继续
线程组任务0结束,其他任务继续
线程执行结束。。。。
public class Test {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("线程组执行结束");
}
});
for (int i = 0; i < 5; i++) {
new Thread(new readNum(i,cyclicBarrier)).start();
}
}
static class readNum implements Runnable{
private int id;
private CyclicBarrier cyc;
public readNum(int id,CyclicBarrier cyc){
this.id = id;
this.cyc = cyc;
}
@Override
public void run() {
synchronized (this){
System.out.println("id:"+id);
try {
cyc.await();
System.out.println("线程组任务" + id + "结束,其他任务继续");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
id:0
id:4
id:3
id:1
id:2
线程组执行结束
线程组任务2结束,其他任务继续
线程组任务0结束,其他任务继续
线程组任务1结束,其他任务继续
线程组任务3结束,其他任务继续
线程组任务4结束,其他任务继续
如果多线程同时运行某段代码,如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
多个线程同时访问相同的资源并进行读写操作时,就有可能产生竞态条件。通俗一点,就是线程A 需要判断一个变量的状态,然后根据这个变量的状态来执行某个操作。而在执行这个操作之前,这个变量的状态可能会被其他线程修改。
最典型的就是单例
public class Singleton {
private static Singleton instance;
public static Singleton getInstance(){
if(instance == null)
instance = new Singleton();
return instance;
}
}
线程a和线程b同时执行getInstance(),线程a看到instance为空,创建了一个新的Obj对象,此时线程b也需要判断instance是否为空,此时的instance是否为空取决于不可预测的时序:包括线程a创建Obj对象需要多长时间以及线程的调度方式,如果b检测时,instance为空,那么b也会创建一个instance对象。
线程B 在执行红色部分代码(判断instance是否为空)时,线程A 还没来得执行完绿色部分的代码。
在后台运行的线程,它的任务是为其他线程提供服务,如果前台线程都死亡,后台线程也会自动死亡。JVM垃圾回收线程就是典型的后台线程。
线程睡眠sleep:调用该sleep()方法的线程进入阻塞状态,睡眠期间内不能返回运行状态(在同步代码块或同步方法上使用,不会释放同步监视器)
线程让步yield:调用该yield()方法的线程暂停,进入就绪状态,但会随时返回运行状态(在同步代码块或同步方法上使用,不会释放同步监视器)
改变线程优先级:每个线程执行时都具有一定的优先级,优先级高的比优先级低的获得更多的执行机会。每个线程的优先级都与创建它的父线程优先级相同,在默认情况下,main线程具有普通优先级,而main线程创建的子线程也具有普通优先级。
synchronized(obj){
… //此处的代码就是同步代码块
}
obj就是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。Java允许使用任何对象作为同步监视器,同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问。
public class ThreadTest extends Thread {
private Account account;
private double money;
public void run() {
synchronized(account){
…
}
}
}
同步方法:使用synchronized关键词修饰某个方法,该方法称为同步方法,同步监视器是this。
public class Account {
private String account;
private double balance;
public synchronized void draw(double drawAmount) {
if(account>= drawAmount){
…
}
}
}
线程执行同步代码块或同步方法时,程序调用sleep()、yield()方法会暂停该线程,但不会释放同步监视器。
同步锁:Lock是控制多个线程对共享资源进行访问的工具。在实现线程安全控制中,比较常用的是ReentrantLock(可重加锁)
public class Account {
private final ReentrantLock lock=new ReentrantLock();
private String account;
private double balance;
public void draw(double drawAmount) {
lock.lock();
try{
if(account>= drawAmount)
{ … }
}finally{
Lock.unlock();
}
}
}
Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着”某条线程”指的是当前线程。
由于wait,notify和notifyAll都是锁级别的操作,而每个对象都有锁,所以把他们定义在Object类中。
这些方法只能在同步方法或者同步代码块里面调用,wait()导致当前线程暂时释放锁,进入阻塞状态(而sleep()、yield()方法不会释放锁),好让其他使用同一把锁的线程有机会执行,锁借出去了,那怎么把锁收回来呢?(线程A调用wait()方法,把锁让线程B运行)
notify()方法唤醒在此同步监视器上等待的单个线程。如果有多个线程在等待,则随机选一个;而notifyAll()唤醒在此同步监视器上等待的所有线程。
public class Business {
private boolean bShouldSub=true;
public synchronized void sub(int i){//子线程
while(!bShouldSub){
try {
this.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
for (int j = 1; j <= 2; j++)
System.out.println("sub thread sequece of "+j+",loop of"+i);
bShouldSub=false;
this.notify();//唤醒主线程
}
public synchronized void main(int i){//主线程
while(bShouldSub){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int j = 1; j <= 4; j++)
System.out.println("main thread sequece of "+j+",loop of"+i);
bShouldSub=true;
this.notify();//唤醒子线程
}
public static void main(String[] args) {
final Business business=new Business();
new Thread(
new Runnable() {
@Override
public void run() {
for(int i=1;i<=2;i++){
System.out.println("sub");
business.sub(i);
}
}
}).start();
for(int i=1;i<=4;i++){
System.out.println("main");
business.main(i);
}
}
}
运行结果:
main
sub
sub thread sequece of 1,loop of1
sub thread sequece of 2,loop of1
sub
main thread sequece of 1,loop of1
main thread sequece of 2,loop of1
main thread sequece of 3,loop of1
main thread sequece of 4,loop of1
main
sub thread sequece of 1,loop of2
sub thread sequece of 2,loop of2
main thread sequece of 1,loop of2
main thread sequece of 2,loop of2
main thread sequece of 3,loop of2
main thread sequece of 4,loop of2
main
死锁是多个线程因争夺资源而造成的一种互相等待的现象,若无外力作用,这些进程都将无法向前推进。(一般是等待对方释放同步监视器)
一个简单的死锁例子:
public class DeadLock implements Runnable {
public int flag = 1;
//静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
new Thread(td1).start();
new Thread(td2).start();
}
}
死锁的发生必须同时满足以下四个条件:
3种用于避免死锁的技术:
与死锁不同,处于活锁的线程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。
死锁:两人在狭窄的胡同相遇,两人都不歉让,等待着对方给自己让路
活锁:两人在狭窄的胡同相遇,两人都试着避让对方,但避让的方向都一样,最后谁都不能通过
饥饿:A、B在狭窄的胡同相遇,A让B先通过,这时来了C,A又让C先通过,如此来了一个又一个,A一直处于等待状态
这是JDK强制的,wait()、notify()、notifyAll()方法都是锁级别的操作,在调用前都必须先获得对象的锁。
wait()方法立即释放对象监视器,notify()、notifyAll()方法则会等待线程执行完,或主动调用wait()方法,才会放弃对象监视器。
可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行,位确保顺序,T3调用T2,T2调用T1。
通过在线程之间共享对象就可以了,然后通过wait()、notify()、notifyAll(),await()、signal()、signalAll()进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。
ThreadLocal主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每个线程中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。(ThreadLocal只能使用Object类型,不能使用基本类型)
但是在如 web 服务器使用要特别小心,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,就存在内存泄露的风险。
public class ThreadLocalDemo implements Runnable {
private final static ThreadLocal studentLocal = new ThreadLocal();
public static void main(String[] agrs) {
ThreadLocalDemo td = new ThreadLocalDemo();
Thread t1 = new Thread(td, "a");
Thread t2 = new Thread(td, "b");
t1.start();
t2.start();
}
public void run() {
accessStudent();
}
public void accessStudent() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running!");
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread " + currentThreadName + " set age to:" + age);
Student student = getStudent();
student.setAge(age);
System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
try {
Thread.sleep(500);
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
}
protected Student getStudent() {
Student student = (Student) studentLocal.get();
//线程首次执行此方法的时候,studentLocal.get()肯定为null
if (student == null) {
student = new Student();
studentLocal.set(student);
}
return student;
}
}
ThreadLocal和Synchonized都用于解决多线程并发访问,区别:
同步集合和并发集合都支持线程安全,主要区别体现在性能和可扩展性。
wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:
synchronized (obj) {
while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}
栈是一块和线程紧密相关的内存区域,每个线程都有自己的栈内存,用于存储本地变量,方法参数,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。
创建线程需要花费昂贵的资源,大量创建线程有可能导致系统内存消耗完,以及“过度切换”。而线程池有效限制并发线程的数量,使得运行效果达到最佳。线程池在系统启动时即创建大量空闲的线程,一个runnable对象传给线程池,线程池就会启动一个线程执行它的run()方法,执行完后,线程不会死亡,而是再次返回线程池成为空闲线程。
作用:
(1)作用:
(2)通过notify()和wait()实现
public class ShareDataTest{
public static void main(String[] args){
Factory f = new Factory();
Producer p = new Producer(f);
Consumer c = new Consumer(f);
new Thread(p).start();
new Thread(c).start();
}
}
class Producer implements Runnable{
private Factory f;
public Producer(Factory f){
this.f = f;
}
@Override
public void run(){
f.produce();
}
}
class Consumer implements Runnable{
private Factory f;
public Consumer(Factory f){
this.f = f;
}
@Override
public void run(){
f.consume();
}
}
class Factory {
private final int MAX_SIZE = 10;//最大储存量
private LinkedList
(3)通过BlockingQueue实现
public class ShareDataTest{
public static void main(String[] args){
Factory f = new Factory();
Producer a = new Producer(f);
Consumer b = new Consumer(f);
Consumer c = new Consumer(f);
new Thread(a).start();
new Thread(b).start();
new Thread(c).start();
}
}
class Producer implements Runnable{
private Factory f;
public Producer(Factory f){
this.f = f;
}
@Override
public void run(){
f.produce();
}
}
class Consumer implements Runnable{
private Factory f;
public Consumer(Factory f){
this.f = f;
}
@Override
public void run(){
f.consume();
}
}
class Factory {
private final int MAX_SIZE = 10; //最大储存量
private BlockingQueue queue=new ArrayBlockingQueue(MAX_SIZE);
public void consume(){
while(true){
try {
Thread.sleep(1000);//模拟耗时
System.out.println(Thread.currentThread().getName()+"正在消费数字"+queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void produce(){
while(true){
try {
Thread.sleep(400);//模拟耗时
int n=new Random().nextInt(1000);
queue.put(n);
System.out.println(Thread.currentThread().getName()+"正在生产数字"+n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
分有界队列和无界队列进行讨论
抢占式的动态优先级调度算法,线程调度程序按线程的优先级进行调度,高优先级的线程先被调度。
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中;而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口。
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
原子性:指操作具有不可分割性,如对基本变量赋值(除long、double外),非原子操作都有安全性问题
可见性:指线程之间的可见性,一个线程修改的状态对另一个线程是可见的,如volatile修饰的变量就具有可见性
例如用volatile int count=1具有可见性和原子性,但 count++ 操作就不是原子性的。
可以创建,不过只是一个指向数组的引用,而不是整个数组,volatile修饰的变量如果是对象或数组之类的,其含义是对象或数组的地址具有可见性,但是数组或对象内部的成员改变不具备可见性。
能,一个典型的例子是读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子操作。
无论你的同步块是正常还是异常退出的,里面的线程都会释放锁。
单例模式:多线程下会产生竞态条件,可能会创建两个实例对象。
public class Singleton {
private static Singleton instance;
public static Singleton getInstance(){
if(instance == null)//1
instance = new Singleton();//2
return instance;//3
}
}
假设两线程同时调用getInstance()
1、线程1先调用getInstance(),在执行//2时被线程2抢占了 2、此时instance=null,线程2进入//1,创建了Singleton对象,并在//2出将变量instance指向该对象,并在//3处返回 3、线程1抢回cpu,在停止的地方继续执行,在//2处创建了另一个Singleton对象 结果是两个线程都创建了对象 |
getInstance():使用同步方法
public class Singleton {
private static Singleton instance;
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
不可否认,synchronized关键字是可以保证单例,但是程序的性能却不容乐观,原因在于getInstance()整个方法体都是同步的,这就限定了访问速度。其实我们需要的仅仅是在首次初始化对象的时候需要同步,对于之后的获取不需要同步锁。
双检锁(模式1):使用同步代码块
public class Singleton {
private static Singleton instance;
public static Singleton getInstance(){
if(instance == null){//1
synchronized (Singleton.class) {//2
instance = new Singleton();//3
}
}
return instance;
}
}
该方法同在多线程下同样会创建两个实例对象,假设两线程同时调用getInstance()
1、线程1先获得cpu进人//1后,被线程2抢占了 2、线程2进入//2获得锁,线程1被阻塞,线程2创建了对象实例 3、线程2退出synchronized块,线程1继续执行,获得锁,并创建了对象实例 结果是两个线程都创建了对象 |
双检锁(模式2):使用同步代码块
针对以上问题,需要对instance进行第二次检查,这就是“双检锁”。
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) {//1
if(instance == null)//2
instance = new Singleton();//3
}
}
return instance;
}
}
该方法既可以实现线程安全,又可以保证性能不受影响,解决了上述的问题,可以说是完美的。但理论是完美的,现实是残酷的。该方法在很多优化编译器上是也不完全安全,原因在于//3的代码在不同编译器上的行为是无法预知的。一个优化编译器可以合法地如下实现 instance=new Singleton():
但JVM并不保证后两个操作的先后顺序。假设两线程同时调用getInstance(),线程A先进入,在执行到//3时JVM可能先为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就有可能出错了。线程A离开synchronized块后,线程B进入,B看到的是instance已经不是null了(内存已经分配),于是它开始放心地使用instance,但这个是错误的,因为A还没有来得及完成instance的初始化,而线程B就返回了未被初始化的instance实例。
解决方法是:变量instance使用volatile修饰,禁止指令重排优化(jdk1.5版本后有效)
Initialization on Demand Holder模式:使用内部类维护单例的实现
该方法使用内部类来做到延迟加载对象,而且其加载过程是线程安全的。这种写法完全使用了JVM的机制,它能保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载。
public class Singleton
{
private static class SingletonHolder{
public final static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
DateFormat的所有实现都不是线程安全的,可以使用ThreadLocal,使得每个线程都将拥有自己的SimpleDateFormat对象副本。
public class DateUtil {
private static ThreadLocal local = new ThreadLocal();
public static Date parse(String str) throws Exception {
SimpleDateFormat sdf = local.get();
if (sdf == null) {
sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
local.set(sdf);
}
return sdf.parse(str);
}
public static String format(Date date) throws Exception {
SimpleDateFormat sdf = local.get();
if (sdf == null) {
sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
local.set(sdf);
}
return sdf.format(date);
}
}
序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘中,或通过网络传输,Java对象可实现Serializable接口或Externalizable接口。Externalizable是Serializable的子类,可以使部分属性实现序列化,一般我们可以用transient代替它。
注:static、transient修饰的数据成员是不能够被序列化的。
(1)输入流和输出流:是从程序运行所在内存的角度来划分的
Java的输入流主要由InputStream和Reader作为基类,而输出流主要由OutputStream和Writer作为基类。它们都是抽象类,无法直接创建实例。
(2)字节流和字符流
用法几乎一样,区别在于字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符;字节流主要由InputStream和OutputStream作为基类,字符流主要由Reader和Writer作为基类。
(3)节点流和处理流
如果进行输入/输出的内容是文本内容,则应该考虑使用字符流;若是二进制内容,则应该考虑使用字节流。
分类 |
字节输入流 |
字节输出流 |
字符输入流 |
字符输出流 |
抽象基类 |
InputStream |
OutputStream |
Reader |
Writer |
访问文件 |
FileInputStream |
FileOutputStream |
FileReader |
FileWriter |
访问数组 |
ByteArrayInputStream |
ByteArrayOutputStream |
CharArrayReader |
CharArrayWriter |
访问管道 |
PipedInputStream |
PipedOutputStream |
PipedReader |
PipedWriter |
访问字符串 |
|
|
StringReader |
StringWriter |
缓冲流 |
BufferedInputStream |
BufferedOutputStream |
BufferedReader |
BufferedWriter |
转换流 |
|
|
InputStreamReader |
OutputStreamWriter |
对象流 |
ObjectInputStream |
ObjectOutputStream |
|
|
抽象基类 |
FilterInputStream |
FilterOutputStream |
FilterReader |
FilterWriter |
打印流 |
|
PrintStream |
|
PrintStream |
推回输入流 |
PushbackInputStream |
|
PushbackReader |
|
特殊流 |
DataInputStream |
OutputStream |
|
|
注:粗体字为节点流,其他为处理流
Java内存由栈内存、堆内存、方法区、本地方法栈和程序计数器组成。
(1)程序计数器(线程私有):是一块较小的内存空间,可看做是当前线程所执行的字节码的行号指示器。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,该内存为线程所私有。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。
(2)Java栈(线程私有):描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
在Java 虚拟机规范中,对这个区域规定了两种异常状况:
(3)本地方法栈:使用native关键字修饰的方法(原生函数),该方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。
本地方法栈与Java栈的作用类似,其区别是Java栈为JVM执行Java 方法(也就是字节码)服务,而本地方法栈则是为JVM使用到的Native 方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。
(4)Java堆(线程共享):用于保存new产生的对象,数组(对象只包含成员变量,不包括成员方法。同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法)
(5)方法区(线程共享):方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量
常量池:存在于方法区中,存放字符串常量和基本类型(指包装类)。
由于不同的对象,生命周期是不一样的,所以分代回收采用分而治之的思想,对不同的对象采用最适合的垃圾回收方式进行回收。
JVM把不同的对象分为三个代:年轻代、年老代和持久代。其中持久代主要存放的是Java类的类信息,与垃圾收集关系不大。由于Java堆是垃圾回收的主要区域,Java堆分为年轻代和年老代,而年轻代又分为Eden区、Survivor from区和Survivor to区。
GC有两种类型:Minor GC和Full GC(Major GC)
Java内存模型描述了线程共享变量的访问规则,以及在JVM中将变量储存到内存和内存中读取出变量这样的底层细节。
线程间的通信机制有两种:共享内存和消息传递
在共享内存的并发模型中,线程间共享程序的公共状态,线程间通过写-读内存中的公共状态来隐式进行通信。
在消息传递的并发模型中,线程间没有公共状态,线程间必须通过明确的发送消息来显式进行通信。
Java线程间通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
线程A与线程B之间如要通信(可见性实现)的话,必须要经历下面2个步骤:
补充概念:
(1)synchronized:除了原子性(同步)外,还有可见性
线程解锁前对共享变量的修改在下次加锁时对其他线程可见。
(2)volatile:实现可见性是通过加入内存屏障和禁止重排序优化来实现的。但volatile没有原子性,因此只能保证变量安全,不能保证线程安全
volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又强迫线程将变化值回写到主内存。这样在任何时刻,不同线程总能看到该变量的最新值。
volatile要在多线程中安全使用,必须满足
因此在实际中,volatile的适用范围是比较窄的。
类的加载是指将类的.class文件的二进制数据读入内存,并为之创建一个Java.lang.Class对象。
类的加载包括包括以下5个阶段:加载、验证、准备、解析、初始化
类的生命周期:加载、验证、准备、解析、初始化、使用、卸载
主要有一下四种类加载器:
详细可参考:http://www.cnblogs.com/ityouknow/p/5603287.html
126、Java程序的初始化顺序是怎样的
执行顺序:父类静态变量 > 父类静态代码块 > 子类静态变量 > 子类静态代码块 > 父类非静态变量 > 父类非静态代码块> 父类构造函数> 子类非静态变量> 子类非静态代码块> 子类构造函数 。
浅复制仅仅复制所考虑的对象,而不复制它所引用的对象;而深复制把复制的对象所引用的对象都复制了一遍。
若存在以下类:
public class BubbleSort {
private int i;
private StringBuffer s;
}
反射机制主要功能:
反射机制获取class对象有三种方法:
(1)创建对象
在很多JavaEE框架中都需要根据配置文件来创建Java对象,从配置文件读取的只是字符串的类名,根据该字符串创建对应的实例,就必须使用映射。
Class c =Class.forName("Employee");
Object o = c.newInstance(); //调用了Employee的默认构造器.
(2)调用方法
通过Class对象的getMethod()或getMethods()方法获得Method对象或Method数组,然后通过Method的invoke()方法来调用对应的方法,相对于执行了目标对象的setter方法。
(Spring框架就是通过这种方式将成员变量以及依赖的对象等都放在配置文件中进行管理的,这也是Spring的IoC的秘密)
(3)访问成员变量
通过Class对象的getFields()或getField()方法可以获取该类所包含的全部Field或指定Field。
(1)静态内部类(可有修饰符)
静态内部类可不依赖于外部类实例而被实例化,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法。
class outerClass{
static class innerClass{ //静态内部类
}
}
(2)成员内部类(可有修饰符)
成员内部类需依赖于外部类实例而被实例化,可访问外部类的任何属性和方法,但成员内部类中不能定义静态成员。
class outerClass{
class innerClass{ //静态内部类
}
}
(3)局部内部类(不能有修饰符)局部内部类跟局部变量一样,不能有修饰符
class outerClass{
public void method1(){
class innerClass{} //局部内部类
}
public static void method2(){
class innerClass{} //局部静态内部类
}
}
(4)匿名内部类(不能有修饰符)
interface Product {
public double getPrice();
public String getName();
}
public class AnonymousTest {
public void test(Product p) {
System.out.println(p.getName()+p.getPrice());
}
public static void main(String[] aegs){
AnonymousTest ta=new AnonymousTest();
ta.test(new Product(){ //匿名内部类
public double getPrice(){
return 42.65;
}
public String getName(){
return "hjy";
}
});
}
}
变量名、函数名、数组名统称为标识符,Java规定标识符只能由字母(a~z,A~Z)、数字(0~9)、下划线(_)、和$组成,并且标识符的第一个·字符必须是字母、下划线或$,此外不能包含空格。
使用switch(expr)时,expr只能是枚举常量(内部由整型或字符类型实现)或一个整型表达式。
instanceof是一个二元运算符,用来判断一个引用类型的变量所指向的对象是否是一个类(接口、抽象类、父类)的实例,即它左边的对象是否是它右边的类的实例。
strictfp用来确保浮点数运算的准确性,没有该关键字,浮点数运算会不精准,并且在不同平台输出结果可能不同。一旦用strictfp声明一个类、接口、方法,在声明的范围内浮点数运算都是精准的。当修饰一个类时,所有方法都会隐式地被strictfp修饰。
值传递(基本数据类型):方法调用中,实参会把值传递给形参,形参与实参有相同的值,但有着不同的存储单元,因此对形参的改变不会影响实参的值。
引用传递(其它所有类型):方法调用中,传递的是对象的地址,这是形参与实参的对象指向同一块存储单元,因此对形参的改变会影响实参的值。
注:基本数据类型的包装类都是不可变量,因此要注意!
不同类型数据在运算时会自动隐式转换,从低精度向高精度转换,即byte < short < char < int < long < float < double;而从高精度向低精度转换时需要强制转换。
注意一下几点:
总之,多种数据混合运算时,系统会先自动将所有数据转换成容量最大的那一种数据类型后,才进行计算。
<<:左移n位表示原值乘以2的n次方,如(4<<3) = ( 4*2^3)
>>(有符号右移运算符):若为正数,则高位补0;若为负数,则高位补1
>>>(无符号右移运算符):不管正负,都会在高位补0
注:byte、char、short类型移位时会自动转化为int型,右移位数超过32bit时,采用取余操作,即a>>n等价于a>>(n%32)
负数以补码的形式存储,i=-5的二进制表示:
原码:00000000 00000000 00000000 00000101 (即绝对值)
反码:11111111 11111111 11111111 11111010 (原码取反)
补码:11111111 11111111 11111111 11111011 (反码+1)
则 i >> 1后的值为-3
—— 11111111 11111111 11111111 11111101(补码)> 11111111 11111111 11111111 11111100 (反码)> 00000000 00000000 00000000 00000011 (原码)
Java的动态绑定又称为运行时绑定,即程序会在运行时自动选择调用哪个方法,是多态实现的一种——方法重写,子类的实例赋值给父类的引用,当执行重写的方法时,执行的是子类的方法。
(1)自旋锁:使线程在没有获得锁时不被挂起,而转去执行一个空循环,若干空循环后,线程如果可以获得锁,则继续执行;若线程依然不能获得锁,才会被挂起。
使用自旋锁的线程被挂起的几率相对减少,执行的连贯性相对加强。适用于锁竞争不是很激烈,锁占用时间很短的并发线程。在锁竞争激烈的情况下,使用自旋锁的线程长时间无法得到对应的锁,不仅白白浪费了CPU时间,最终还免不了被挂起。
(2)阻塞锁:使线程进入阻塞状态,当获得相应的信号(唤醒,时间) 时,才可以进入就绪状态,然后通过竞争,进入运行状态。
JAVA中,能够进入/退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字、ReentrantLock、Object.wait()\notify()。
(3)可重入锁:也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁,它最大的作用是避免死锁。
(4)悲观锁和乐观锁
(5)读写锁:如ReentrantReadWriteLock
可以,前提是实体类必须有明确的构造函数,即抽象类的构造函数是有(无)参的,实体类也必须有有(无)参的构造函数。
new创建对象的方式称作为静态加载,而使用Class.forName("XXX")称作为动态加载,它们俩本质的区别在于静态加载的类的源程序在编译时期加载(必须存在),而动态加载的类在编译时期可以缺席(源程序不必存在)。
静态加载类
如果不定义A类和B类,则会出现编译错误。假想如下场景,Test中有100个功能,你只想使用A功能,如果你使用的是静态加载的机制,你必须定义其余的99种功能才能使用A功能。
public class Test{
public static void main(String[] args){
if("A".equals(args[0])){
new A().print();
}
if("B".equals(args[0])){
new B().print();
}
}
}
动态加载类
如果使用动态加载机制,不仅不用定义99中功能,通过实现某种标准(继承某个接口),将大大方便了代码的编写。
public class Test{
public static void main(String[] args){
try{
Class c = Class.forName(args[0]);
MyStandard me = (MyStandard)c.newInstance();
me.print();
}catch(Exception e){
e.printStackTrace();
}
}
}
interface MyStandard{
public void print();
}
public class A implements MyStandard{
public void print(){
System.out.println("A");
}
}
public class B implements MyStandard{
public void print(){
System.out.println("B");
}
}
Lambda表达式的基本语法:(parameters) -> expression 或 (parameters) ->{ statements; }
Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) ); Arrays.asList( "a", "b", "d" ).forEach(System.out::println);
//使用匿名内部类 new Thread(new Runnable() { @Override public void run() { System.out.println("Hello world !"); } }).start(); //使用 lambda expression new Thread(() -> System.out.println("Hello world !")).start();
Arrays.sort(players, new Comparator
() { @Override public int compare(String s1, String s2) { return (s1.compareTo(s2)); } }); //使用 lambda expression 排序 Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2)));
默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写。默认方法的出现提供了在不破坏接口的兼容性的前提下对接口进行扩展。
使用与一般Java类的静态方法一样
public interface AA {
//静态方法
static void helloWorld(){
System.out.println("hello world");
}
//默认方法
default void doSomething(){
System.out.println("doSomething in AA");
}
}
public interface BB extends AA{
//重新了父接口的默认方法
default void doSomething(){
System.out.println("doSomething in CC");
//只有直接父类才可以通过XX.super.xxMethod()的方式调用父类默认方法
AA.super.doSomething();
}
}
public class Test1 implements BB{
public static void main(String[] args) {
BB bb = new Test1();
bb.doSomething();
AA.helloWorld();
}
}
作用:
例如:
类: