Java疑难杂问①

Java遍历集合的方法

循环,迭代(使用迭代器iterator),加强for循环和forEach。

  加强for循环实际上在背后使用的是迭代器。这意味着编译时Java编译器会将增强型for循环语法转换为迭代器构造。

forEach方法与之前的方法最大的区别是什么?
  在之前的方法中(经典for循环,迭代器和加强for循环),程序员可以控制集合是如何迭代的。迭代代码不是集合本身的一部分,它是由程序员编写的,因此称为外部迭代。相比之下,新方法将迭代代码封装在集合本身中,因此程序员不必为迭代集合编写代码,相反,程序员会在每次迭代中指定要做什么。 因此属于内部迭代:集合处理迭代本身,而程序员传递动作 - 即每次迭代需要做的事情。

迭代器和for循环的效率比较

记录的存取方式有两种:一种是顺序存储(数组,ArrayList)可以根据其下标找到对应的记录,另一种是链接存储(LinkedList)链接存储则必须找到其前一个记录的位置才能够找到本记录。for循环便于访问顺序存储的记录,而foreach和迭代器便于访问链接存储。

编译与链接

(1)编译:由编译程序将用户的源代码编译成若干个目标模块。可以理解为将高级语言翻译为计算机可以理解的二进制代码,即机器语言。
(2)链接:由链接程序将编译后形成的一组目标模块以及它们所需要的库函数链接在一起,形成一个完整的载入模块。

一般高级语言程序编译的过程:预处理、编译、汇编、链接。
编译程序的工作过程一般划分为五个阶段:词法分析、语法分析、语义分析与中间代码产生、优化、目标代码生成。

final, finally, finalize关键字的作用和区别

(1)final:可以作为修饰符修饰变量、方法和类,被final修饰的变量只能一次赋值;被final修饰的方法不能够在子类中被重写(override);被final修饰的类不能够被继承。

(2)finally用在异常处理中定义总是执行代码,无论try块中的代码是否引发异常,catch是否匹配成功,finally块中的代码总是被执行,除非JVM被关闭(System.exit(1)),通常用作释放外部资源(不会被垃圾回收器回收的资源)。

(3)finalize()方法是Object类中定义的方法,当垃圾回收器将无用对象从内存中清除时,该对象的finalize()方法被调用。由于该方法是protected方法,子类可以通过重写(override)该方法以整理资源或者执行其他的清理工作。(有可能对象会复活)

泛型擦除

  泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,即所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率。
  Java 的泛型是伪泛型, 运行期的泛型类型,被擦除了,因此,在运行期,ArrayList< String> 和 ArrayList< Integer> 是相同的类型。JVM并不知道泛型的存在,因为泛型在编译阶段就已经被处理成普通的类和方法,处理机制是通过类型擦除,擦除规则:

  • 若泛型类型没有指定具体类型,用Object作为原始类型
  • 若有限定类型< T exnteds XClass >,使用XClass作为原始类型
  • 若有多个限定< T exnteds XClass1 & XClass2 >,使用第一个边界类型XClass1作为原始类型

泛型擦除的体现
  在写代码时,无法把一个 String 类型的实例加到 ArrayList< Integer> 中,因为ArrayList< Integer> 和 ArrayList< String> 在编译的时候是完全不同的类型,但是运行结果却是true。这就Java泛型的类型擦除造成的。因为不管是 ArrayList< Integer> 还是 ArrayList< String>,在编译完成后都会被编译器擦除成了 ArrayList。Java 泛型擦除是 Java 泛型中的一个重要特性,是指在编译后的字节码文件中类型信息被擦除,变为原生类型(raw type),其目的是避免过多的创建类而造成的运行时的过度消耗。所以,像ArrayList< Integer> 和 ArrayList< String> 这两个实例,其类实例是同一个。

二叉树和堆的区别

  • 在二叉排序树中,每个结点的值均大于其左子树上所有结点的值,小于其右子树上所有结点的值,对二叉排序树进行中序遍历得到一个有序序列;堆是一个完全二叉树,并且每个结点的值都大于或等于其左右孩子结点的值(这里的讨论以大顶堆为例)
  • 具有n个结点的二叉排序树,其深度取决于给定集合的初始排列顺序,最好情况下其深度为log n(表示以2为底的对数),最坏情况下其深度为n;具有n个结点的堆,其深度即为堆所对应的完全二叉树的深度log n
  • 在二叉排序树中,某结点的右孩子结点的值一定大于该结点的左孩子结点的值;在堆中却不一定,堆只是限定了某结点的值大于(或小于)其左右孩子结点的值,但没有限定左右孩子结点之间的大小关系
  • 在二叉排序树中,最小值结点是最左下结点,其左指针为空;最大值结点是最右下结点,其右指针为空。在大根堆中,最小值结点位于某个叶子结点,而最大值结点是大顶堆的堆顶(即根结点)
  • 二叉排序树是为了实现动态查找而设计的数据结构,它是面向查找操作的,在二叉排序树中查找一个结点的平均时间复杂度是O(log n);堆是为了实现排序而设计的一种数据结构,它不是面向查找操作的,因而在堆中查找一个结点需要进行遍历,其平均时间复杂度是O(n)

Java反射

  Java反射机制核心是JVM在运行的时候才动态加载类,并且对于任意一个类,都能够知道这个类的所有属性和方法,调用方法/访问属性,不需要提前在编译期知道运行的对象是谁,允许运行中的Java程序获取类的信息,并且可以操作类或对象内部属性。
  反射的原理:JAVA语言编译之后会生成一个.class文件,这些 Class 对象承载了这个类型的父类、接口、构造函数、方法、属性等原始信息,这些 class 文件在程序运行时会被 ClassLoader 加载到虚拟机中。反射就是通过字节码文件找到某一个类、类中的方法以及属性等。

获取反射入口(class对象)的三种方法:
1、通过Class.forName(“全类名”)

try {
		Class<?> perClazz = Class.forName("reflect_fanshe.Person");
		System.out.println(perClazz);
	} catch (ClassNotFoundException e) {			
			e.printStackTrace();
	}

2、类名.class

Class<?> perClazz2 = Person.class;

3、对象.getClass()

Person person = new Person();
Class<?> perClazz3 = person.getClass();

通过反射来生成对象有两种方式:

  1. 通过Class对象的newInstance()方法来创建Class对象对应类的实例。这个方法是使用Class对象对应类的默认构造器创建对象,这就要求Class对象对应类必须要有默认构造器。此种方法比较常见,在spring这些框架中,会根据配置文件自动创建类的实例并注入到依赖此类的类中。这时候用的最多的就是默认构造器。像是在spring的配置文件中,提供的是某个类的全类名,这种情况要创建类的实例,就必须使用反射了
  2. 使用Class对象获取指定的Constructor对象,调用Constructor对象的newInstance()方法来创建Class对象对应类的实例。这个方法可以使用Class对象对应类的任意指定的构造器来创建实例

Java中Class.forName和ClassLoader的区别

Java中Class.forName和classloader都可以用来对类进行加载,不同点:

  1. Class.forName除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块
  2. 而classloader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块
  3. Class.forName(name,initialize,loader)带参数也可控制是否加载static块并且只有调用了newInstance()方法才会调用构造函数,创建类的对象

  Class.forName(className)方法,内部实际调用的方法是Class.forName(className,true,classloader);第2个boolean参数表示类是否需要初始化, Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化。
  ClassLoader.loadClass(className)方法,内部实际调用的方法是ClassLoader.loadClass(className,false);第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行

java中同步锁修饰同一个类的两个方法的理解

  一个类中的两个方法都加了同步锁,多个线程能同时访问这个类的两个方法吗?这个问题要分成synchronized和ReentrantLock两个情况:

一.对于synchronized
1.一个类中的两个方法都加了同步锁,多个线程不能同时访问这个类的同一实例对象的两个方法
2.一个类中的两个方法都加了同步锁,多个线程能同时访问这个类的不同实例对象的两个方法
3.一个类中的两个方法(Runnable的run方法)都加了同步锁,多个线程能同时访问这个类的两个方法(不论是不是同一实例对象)。因为synchronized(this)中锁住的是代码块,即这个锁在run方法中,但是并不是同步了这个run方法,而是括号中的对象this,即多个线程会拿到各自的锁,就能够同时执行run方法。
4.两个方法都是静态方法且还加了synchronized修饰,多个线程不能同时访问这个类的两个方法(不论是不是同一实例对象,作用的对象是这个类的所有对象)

二.对于ReentrantLock
1.一个类中的两个方法都加了同步锁,多个线程不能同时访问这个类的同一实例对象的两个方法(不论同步加在实例方法中或是run方法中)
2.一个类中的两个方法都加了同步锁,多个线程能同时访问这个类的不同实例对象的两个方法(不论同步加在实例方法中或是run方法)

static代码块、static变量、非static方法和构造器执行顺序

  static修饰的成员是跟着类走的,普通方法和成员变量是跟随对象走的。当类编译加载完成后还没有创建对象,这个时候static常量、static变量、static代码块都是在类“初始化”阶段的时候就分配好内存初始化成功了,而普通的变量和方法是在类的生命周期中的“使用”阶段才创建。静态代码块的代码只会在类第一次初始化的时候执行一次。

执行顺序:
父类的静态内容—>子类的静态内容—>父类非静态代码块—>父类构造器—>
子类非静态代码块—>子类构造器

总结:

  1. static相关的东西在Java类的生命周期中是在“初始化”阶段就开始初始化,所以比非静态的东西先执行
  2. 非静态的东西在Java类的生命周期中是在“使用”阶段创建了对象使用的时候才执行,所以肯定在静态相关的东西之后执行
  3. 静态代码块和静态变量的执行顺序是根据代码书写的先后顺序执行的,尽量把静态变量书写在静态代码块之前
  4. 非静态代码块是在构造器之前先执行的,经常用来对对象做一些公共的初始化操作

java浅复制和深复制

浅复制(浅克隆)
  创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

深复制(深克隆)
  创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。

Java的clone()方法

  • clone方法将对象复制了一份并返回给调用者。一般而言,clone() 方法满足:
    1、对任何的对象x,都有x.clone() !=x //克隆对象与原对象不是同一个对象
    2、对任何的对象x,都有x.clone().getClass() == x.getClass() //克隆对象与原对象的类型一样
    3、如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立
  • Java中对象的克隆
    1、为了获取对象的一份拷贝,我们可以利用Object类的clone()方法
    2、在派生类中覆盖基类的clone()方法,并声明为public
    3、在派生类的clone()方法中,调用super.clone()。在派生类中覆盖Object的clone()方法时,为什么一定要调用super.clone()呢?在运行时,Object中的clone()识别出你要复制的是哪一个对象,然后为此对象分配空间,并进行对象的复制,将原始对象的内容一一复制到新对象的存储空间中
    4、在派生类中实现Cloneable接口

实现深拷贝的方法:
浅拷贝的实现需要实现 Cloneable 接口,并覆写 clone() 方法。Object 类提供的 clone 是只能实现浅拷贝的,那么实现深拷贝有二种思路:
 ①让每个引用类型属性内部都重写clone() 方法:既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝
 ②利用序列化:因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的

多线程轮流打印

三个线程

【1】使用volatile变量

import java.util.concurrent.atomic.AtomicInteger;
public class Test {
    private static volatile int flag = 0;
    private static AtomicInteger i=new AtomicInteger(0);

    static class Thread1 implements Runnable{
        public void run() {
            while(i.get() < 100){
                if(flag==0) {
                    System.out.println("t1="+i);
                    i.getAndIncrement();
                    flag=1;
                }
            }
        }
    }
    static class Thread2 implements Runnable{
        public void run() {
            while(i.get()<100){
                if(flag==1){
                    System.out.println("t2="+i);
                    i.getAndIncrement();
                    flag=2;
                }
            }
        }
    }
    static class Thread3 implements Runnable{
        public void run() {
            while(i.get()<100){
                if(flag==2){
                    System.out.println("t3="+i);
                    i.getAndIncrement();
                    flag=0;
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread thread1=new Thread(new Thread1());
        Thread thread2=new Thread(new Thread2());
        Thread thread3=new Thread(new Thread3());
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

【2】synchronized关键字/ReentrantLock实现

//synchronized关键字实现
	static class MyThread implements Runnable{
        private static Object lock = new Object();
        private static int count =0 ;
        int no;
        public MyThread(int no){
            this.no = no;
        }
        @Override
        public void run() {
            while (count < 100) {
                synchronized (lock) {
                    if (count % 3 == this.no) {
                        System.out.println(this.no + "--->" + count);
                        count++;
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    lock.notifyAll();
                }
            }
        }
    }

//ReentrantLock实现
	static class MyThread implements Runnable {
        int flag;
        private static int count = 0;
        private static Lock lock = new ReentrantLock();
        private static Condition condition = lock.newCondition();
        public MyThread(int n) {
            flag = n;
        }
        @Override
        public void run() {
            while (count < 100) {
                lock.lock();
                if (count % 3 == flag) {
                    System.out.println("线程" + flag + ":" + count);
                    count ++;
                } else {
                    try {
                        condition.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                condition.signalAll();
                lock.unlock();
            }
        }
    }

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MyThread2(0));
        Thread t2 = new Thread(new MyThread2(1));
        Thread t3 = new Thread(new MyThread2(2));
        t1.start();
        t2.start();
        t3.start();
    }
}

如何终止一个正在运行的线程

在java中有以下3种方法可以终止正在运行的线程:
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法
3、使用interrupt方法中断线程

interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程。
Thread.stop()
  该方法天生是不安全的。目标线程可能持有一个监视器,假设这个监视器控制着某两个值之间的逻辑关系,如var1必须小于var2,某一时刻var1等于var2,本来应该受保护的逻辑关系,不幸的是此时恰好收到一个stop命令,产生一个ThreadDeath错误,监视器被解锁。这就导致逻辑错误,当然这种情况也可能不会发生,是不可预料的。

thread.interrupt()
Thread类中有三个方法:

public void Thread.interrupt() // 无返回值
public boolean Thread.isInterrupted() // 有返回值
public static boolean Thread.interrupted() // 静态,有返回值

public void interrupt() {}中断本线程。无返回值。具体作用分以下几种情况:

  • 如果该线程正阻塞于Object类的wait()、wait(long)、wait(long, int)方法,或者Thread类的join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)方法,则该线程的中断状态将被清除,并收到一个java.lang.InterruptedException
  • 如果该线程正阻塞于interruptible channel上的I/O操作,则该通道将被关闭,同时该线程的中断状态被设置,并收到一个java.nio.channels.ClosedByInterruptException
  • 如果该线程正阻塞于一个java.nio.channels.Selector操作,则该线程的中断状态被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用java.nio.channels.Selector.wakeup()方法一样
  • 如果上述条件都不成立,则该线程的中断状态将被设置。

小结:第一种情况最为特殊,阻塞于wait/join/sleep的线程,中断状态会被清除掉,同时收到InterruptedException;而其他情况中断状态都被设置,并不一定收到异常。

  中断一个不处于活动状态的线程不会有任何作用。如果是其他线程在中断该线程,则java.lang.Thread.checkAccess()方法就会被调用,这可能抛出java.lang.SecurityException。

public static boolean interrupted() {}
  检测当前线程是否已经中断,是则返回true,否则false,并清除中断状态。换言之,如果该方法被连续调用两次,第二次必将返回false,除非在第一次与第二次的瞬间线程再次被中断。如果中断调用时线程已经不处于活动状态,则返回false。

public boolean isInterrupted() {}
  检测当前线程是否已经中断,是则返回true,否则false。中断状态不受该方法的影响。如果中断调用时线程已经不处于活动状态,则返回false。

  interrupted()与isInterrupted()的唯一区别是,前者会读取并清除中断状态,后者仅读取状态。在hotspot源码中,两者均通过调用的native方法isInterrupted(boolean)来实现,区别是参数值ClearInterrupted不同。

Java中object有哪些方法

getClass()、hashCode()、equals()、toString() 、clone()、wait()、notify()、notifyAll()、finalize()

System.gc()

  执行System.gc()函数的作用只是告诉虚拟机,希望进行一次垃圾回收。至于什么时候进行回收还是取决于虚拟机,而且也不能保证一定进行回收。System.gc()回收没有被任何可达变量指向的对象。JDK实现:

public static void gc() {
    Runtime.getRuntime().gc();
}

调用了Runtime类的gc方法

public native void gc();

Runtime类的gc方法是个native方法,JVM实现:

JNIEXPORT void JNICALL
Java_java_lang_Runtime_gc(JNIEnv *env, jobject this)
{
    JVM_GC();
}

直接调用了JVM_GC()方法,在jvm.cpp中实现的。以此找下去,会发现,System.gc()会触发Full GC。Full GC 耗时比较长,对应用影响较大,一般不推荐使用System.gc()。在有使用堆外内存的情况下,如果堆外内存申请不到足够的空间,jdk会触发一次System.gc()来进行回收。

线程可重入

  有一类线程安全函数叫做可重入函数(reentrant function),其特点:当它们被多个线程调用时,不会引用任何共享数据,多个执行流反复执行一个代码,其结果不会发生改变。可重入函数包含两类:显式可重入函数(explicitly reentrant function)和隐式可重入函数(implicitly reentrant function)。可重入函数满足条件:
(1)不使用全局变量或静态变量
(2)不使用用malloc或者new开辟出的空间
(3)不调用不可重入函数
(4)不返回静态或全局数据,所有数据都有函数的调用者提供
(5)使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

显式可重入函数
  如果所有的函数参数都是值传递的并且所有的数据引用都是本地的自动栈变量(即没有引用全局或静态变量),那么函数就是显示可重入的。

隐式可重入函数
  如果允许显式可重入函数中的一些参数是引用传递的,那么就得到一个隐式可重入函数。如果调用线程小心地传递指向非共享数据的引用,那么它是可重入的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值