2020年Android面试题三部曲——Java部分

2020年Android面试题三部曲——Java部分

    • 常问JAVA知识点
      • 基本数据类型
      • abstract和interface的区别
      • 重写和重载的区别
      • break、continue、return区别
      • 面向对象的特征
      • ==和equals()的区别
      • String、StringBuffer、StringBuilder的区别
      • 正则表达式
      • trim
      • 单例模式
      • 理解创建对象的三个步骤
      • 关于this是我的理解
      • static 的含义
      • 它们是死敌吗
      • final
      • finalize()
      • java 数组的赋值
      • Volatile
    • Java 常问集合
      • 常用集合分类
      • Collections.synchronizedList和vector相比
      • HashMap jdk 1.7 和1.8 的差异
      • concurrentHashMap JDK1.7和1.8比较
      • HashTable和HashMap区别
      • HashSet和HashMap的区别
    • Java面试常问IO
      • 概念
      • NIO和IO的区别
        • NIO真正优势体现在:
      • IO流的两种分类方式
      • Java.io包中六大类和接口
    • 面向对象的六大原则
    • 线程和进程
      • 线程的生命状态
      • Java 提供了三种创建线程的方法
      • 进程和线程的区别:
      • run和start的区别
      • sleep和pield的区别
      • 打断线程的终止方式
      • XML的三种原生解析方式(DOM,SAX,PULL)
      • 线程池相关问题
      • 深入理解TCP/IP协议
    • 类加载机制
      • 类加载器
    • JAVA内存模型
    • 反射
    • 泛型
    • 序列化
    • 几种常用的 sql 的关键字
    • 必会排序算法

在现公司三年了,Android项目写的越来越少了,最近效率比较高,决定重新拾起Android。一个好的结束意味着一个更好的开始。本篇文章是Android面试中的java部分,结合自身情况来写的,部分题目答案简单。
另外两篇:Android部分 、 Android框架部分正在整理中

常问JAVA知识点

基本数据类型

byte short int long char float double boolean
大数据精准类型:bigInterger bigDecimal ------compareTo方法来比较大小
##扩展——保留两位小数(常用两种)
#DecimalFormat

public static String format(double value) {
 DecimalFormat df = new DecimalFormat("0.00");
 df.setRoundingMode(RoundingMode.HALF_UP);//当前舍入模式8种---向“最接近的”整数舍入
 //FLOOR:向远离零的方向舍入。
   DOWN:向接近零的方向舍入。
 return df.format(value);
}

#NumberFormat

public static String format(double value) {

 NumberFormat nf = NumberFormat.getNumberInstance();
 nf.setMaximumFractionDigits(2);
 /*
  * setMinimumFractionDigits设置成2
  * 
  * 如果不这么做,那么当value的值是100.00的时候返回100
  * 
  * 而不是100.00
  */
 nf.setMinimumFractionDigits(2);
 nf.setRoundingMode(RoundingMode.HALF_UP);
 /*
  * 如果想输出的格式用逗号隔开,可以设置成true
  */
 nf.setGroupingUsed(false);
 return nf.format(value);
}

abstract和interface的区别

  • 抽象类 可以有抽象方法和普通方法 只能单继承 提高代码的复用性
  • 接口 只可能有成员变量 和抽象方法 可以多实现 规范代码和提高代码的扩展性

重写和重载的区别

  • 重写发生在继承关系中 方法名相同 参数相同 返回值类型相同 访问权限修饰符不能比父类更加严格
  • 重载是同一个类中 方法名相同

break、continue、return区别

  • break 退出当前循环体,执行循环外的操作
  • continue 退出本次循环 继续执行下一次循环
  • return 退出当前循环体,返回到方法调用处

面向对象的特征

  • 封装、继承、多态
  • 多态:继承、重写、父类引用指向子类对象

==和equals()的区别

  • ==是判断栈内存中存放的对象在堆中的内存地址。也就是判断两个对象是不是相等。
  • equals() 是判断两个对象的内容是不是相同。由于所有的对象都是继承java.lang.object 如果没有对该方法进行覆盖,则调用的是Object类的方法,Object中的equals()返回的是==的判断

String、StringBuffer、StringBuilder的区别

  • String 为字符串常量,每次赋值都是重新创建对象
  • StringBuffer 中的很多方法带有synchronized关键字 线程是安全的
  • StringBuilder 效率高

正则表达式

Pattern pattern =Pattern.complie(String regex)
String regex=pattern.pattern();得到正则表达式
Matcher matcher=pattern.matcher( String imput);
Pattern pattern=matcher.pattern();返回创建mathcer的pattern实例;
matcher.matcher();完全匹配
matcher.find();局部匹配

trim

String.Trim()方法就是把字符串两端的空格字符给删去

单例模式

  • 懒汉式(什么时候用什么时候new)
public class Singleton {
    private static Singleton INSTANCE;
    private Singleton (){}

    public static Singleton getSingleton() {
        INSTANCE = new Singleton();
        return INSTANCE;
    }

}

  • 恶汉式(进来就new)
public class Singleton {
    private static Singleton INSTANCE =  new Singleton(); 
    private Singleton (){}
    public static Singleton getSingleton() {
        return INSTANCE;
    }

}
  • 双向校验锁
public class Singleton {
    private **volatile**  static Singleton INSTANCE; 
    private Singleton (){}

    public static Singleton getSingleton() {
        if (INSTANCE == null) {                         
            synchronized (Singleton.class) {
                if (INSTANCE == null) {       
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

}

  • 枚举
public class BeanContainer {

    public static BeanContainer getInstance() {
        return ContainerHolder.HOLDER.instance;
    }

    private enum ContainerHolder {
        HOLDER;
        private BeanContainer instance;

        ContainerHolder() {//在构造中实例化
            instance = new BeanContainer();
        }
    }
}
  • 静态内部类
public class SingleTon{
  private SingleTon(){}
 
  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }
  
  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
 }
 
}

要理解静态内部类方式,首先要理解类加载机制。(面试中高级很有可能问到)

虚拟机把Class文件加载到内存,然后进行校验,解析和初始化,最终形成java类型,这就是虚拟机的类加载机制。加载,验证,准备,解析、初始化这5个阶段的顺序是确定的,类的加载过程,必须按照这种顺序开始。这些阶段通常是相互交叉和混合进行的。解析阶段在某些情况下,可以在初始化阶段之后再开始—为了支持java语言的运行时绑定(动态绑定,多态的原理)。

在Java虚拟机规范中,没有强制约束什么时候要开始加载,但是,却严格规定了几种情况必须进行初始化(加载,验证,准备则需要在初始化之前开始):

1. 遇到 new、getstatic、putstatic、或者invokestatic 这4条字节码指令,如果没有类没有进行过初始化,则触发初始化

2. 使用java.lang.reflect包的方法,进行反射调用的时候,如果没有初始化,则先触发初始化

3. 初始化一个类时候,如果发现父类没有初始化,则先触发父类的初始化

我们仅说与本期主题相关的初始化阶段:

类初始化阶段是类加载过程的最后阶段。在这个阶段,java虚拟机才真正开始执行类定义中的java程序代码。在编译的时候,编译器会自动收集类中的所有静态变量(类变量)和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是根据语句在java代码中的顺序决定的。收集完成之后,会编译成java类的
static{} 方法,java虚拟机则会保证一个类的static{}
方法在多线程或者单线程环境中正确的执行,并且只执行一次。在执行的过程中,便完成了类变量的初始化。如果我们的java类中,没有显式声明static{}块,如果类中有静态变量,编译器会默认给我们生成一个static{}方法。

对于静态变量来说,虚拟机会保证在子类的static{}方法执行之前,父类的static{}方法已经执行完毕(即如果父类没有加载则先加载父类)。由于父类的static{}方法先执行,也就意味着父类的静态变量要优先于子类的静态变量赋值操作。

对于实例变量来说,在实例化对象时,JVM会在堆中为对象分配足够的空间,然后将空间清零(即所有类型赋默认值,引用类型为null)。JVM会收集类中的复制语句放于构造函数中执行,如果没有显式声明构造函数则会默认生成一个构造函数。子类默认生成的构造函数第一行默认为super();即如果父类有无参的构造方法,子类会先调用父类的构造方法再调用本身的构造方法。因为它继承父类成员的使用,必须先初始化这些成员。如果父类没有无参的构造方法则子类继承会报错,需要子类通过super显式调用父类的有参构造方法。如果类中显式定义一个或多个构造方法,则不再生成默认构造方法。

对于静态变量,上面的描述还不太准确。类初始化阶段,JVM保证同一个类的static{}方法只被执行一次,这是静态内部类单例模式的核心。JVM靠类的全限定类名以及加载它的类加载器来唯一确定一个类。(这个很重要,经常会有这方面的坑!比如反序列化时,被序列化的对象使用java默认的类加载器加载,而使用了反序列化的一方使用的框架(如springBoot就有自己的类加载器)强制使用自己的类加载器加载某个类,则会因为JVM判定不是一个类而报ClassNotFoundException!)

所以修正一下的说法便是,静态内部类单例模式的核心原理为对于一个类,JVM在仅用一个类加载器加载它时,静态变量的赋值在全局只会执行一次!

使用静态内部类的优点是:因为外部类对内部类的引用属于被动引用,不属于前面提到的三种必须进行初始化的情况,所以加载类本身并不需要同时加载内部类。在需要实例化该类是才触发内部类的加载以及本类的实例化,做到了延时加载(懒加载),节约内存。同时因为JVM会保证一个类的()方法(初始化方法)执行时的线程安全,从而保证了实例在全局的唯一性。

有些人–为什么使用内部类而不是直接使用静态变量,我觉着有两个原因(求指正,第二条并不是很确定,后续会写代码测试):

1. 使用内部类可以延时加载。如果直接使用静态变量,因为加载子类等其它原因对实例进行了初始化,而此时并不需要该类的实例,造成了资源的浪费。

2. 原类因为带有业务含义,在使用上会有各种可能,比如使用了特定的类加载器进行加载,这样就对单例造成了破坏。

说完了优点我们再来说说缺点,那就是内部类的传参不是很灵活,需要将参数定义为final。当然我们也可以将其写入final的Object数组或者在内部类定义一个接受参数的init()方法来接收参数,但总的来说传参确实不方便。
原文地址:点击进入原文

理解创建对象的三个步骤

1.在堆内存开辟内存空间。
2.在堆内存中实例化对象里面的各个参数。
3.把对象指向堆内存空间。

关于this是我的理解

1.用来区分当前对象
2.构造方法中调用本身其它构造
3.用来返回类的引用

static 的含义

static 方法是类方法,先于任何的实例(对象)存在。即static 方法再类加载时就已经存在了,但是对象是在创建时才在内存中生成。
static方法就是没有this的方法

它们是死敌吗

在方法中定义使用的this关键字,它的值是当前对象的引用。也就是说你只能用它来调用属于当前对象的方法或者使用this处理方法中成员变量和局部变量重名的情况。而且更为重要的是this和super都无法出现在static修饰的方法中,static修饰的方法是属于类的,该方法的调用者可能是一个类,而不是对象,如果使用的是类来调用而不是对象,则this就无法指向合适的对象,所以static修饰的方法中不能使用this

final

修饰类——当用final去修饰一个类的时候,表示这个类不能被继承。
a.被final修饰的类,final类中的成员变量可以根据自己的实际需要设计final。
b.final类中的成员方法都会被隐式的指定为final方法
修饰方法——被final修饰的方法不能被重写
修饰成员变量
a.必须初始化值
b.被final修饰的成员变量赋值有两种方式 1.直接赋值 2.全部在构造方法中附初始值
c.如果修饰的成员变量是一个引用类型,则是说这个引用的地址的值不能修改,但是这个引用所指向的对象里面的内容还是可以改变的
注意:
a.一个类的private 方法会被隐式的被指定为final方法
b.如果父类中有final修饰的方法,那么子类不能去重写

finalize()

  • finalize() 实在java.lang.Object里定义的,也就是说没一个对象都有这么一个方法。这个方法在gc启动,该对象被回收的时候被调用。

  • 使用finalize 还需要注意一个事,调用super.finalize();

  • 一个对象的finalize()方法只会被调用一次,而且finalize被调用后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再被调用,产生问题。所以使用的时候一定要注意

  • object定义—— protected void finalize()//定义为子类可见

  • 执行时机不可预知
    当一个对象变得不可触及时,垃圾回收器某个时期会回收此对象。当回收对象之前会调用finalize方法,这就类似于人类临终之前要做的事:写遗言。因为GC是不确定的(这和JVM有关)。所以finalize方法的执行具有不可预知性。

  • finalize 忽略异常——即finalize代码中若出现异常,异常会被忽略

  • finalize使用
    什么时候使用?一般来说,finalize被作为第二种安全网来使用,如FileInputStream类,
    当对象回收时,有可能资源为释放,所以这里第二次来确认(那也总比不释放强吧,虽然具体释放时机未定)

protected void finalize() throws IOException { 
    if (fd != null) { 
        if (fd != fd.in) { 
            close(); 
        } 
    } 
} 

注意:某些用到finalize的地方,你必须像如下所示,显式调用回收链。

protected void finalize() throws IOException { 
    try{ 
            ... 
    }finally{ 
            super.finalize(); 
    } 
} 

建议:尽量不要使用finalize,除非以它作为安全网,或是为了终结非关键的原生资源。

java 数组的赋值

int a1= {1,2,3} ; int a2 ; a2= a1 如果改变a2的值a1也会变,其实真正做的只是复制了一个引用,操作的是同一个对象。

Volatile

(1)关键字的作用:保证了变量的可见性(visibility)和防止指令重排序。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。防止指令重排:在基于偏序关系的Happens-Before内存模型中,指令重排技术大大提高了程序执行效率
原理:获取JIT(即时Java编译器,把字节码解释为机器语言发送给处理器)的汇编代码,发现volatile多加了lock addl指令,这个操作相当于一个内存屏障,使得lock指令后的指令不能重排序到内存屏障前的位置。这也是为什么JDK1.5以后可以使用双锁检测实现单例模式。
lock前缀的另一层意义是使得本线程工作内存中的volatile变量值立即写入到主内存中,并且使得其他线程共享的该volatile变量无效化,这样其他线程必须重新从主内存中读取变量值。
性能:读操作与普通变量无差别,写操作会慢一些,大多情况比锁消耗低。

(2)为什么会出现脏读?
Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。变量的值何时从线程的工作内存写回主存,无法确定。
(3)happens-before规则的理解与勘误
在网上查volatile关键字相关信息时,多篇博客提到了happens-before原则,个人对此原则的理解是:当操作该volatile变量时,所有前序对该变量的操作都已完成(如不存在已变更,但未写回主存的情况),所有后续对该变量的操作,都未开始。仅此而已。

Java 常问集合

常用集合分类

Collection 接口的接口 对象的集合(单列集合)

  • List 接口:元素按照先后有序保存,可重复
    -——LinkedList 链表,插入和删除效率高,不同步(不安全) JDK1.7之前双向循环列表,现在是双向链表
    其实就是headerEntry循环链表被替换成了first和last组成的非循环链表。
    1.双向链表好处:在初始化的时候,不用去new一个Entry。
    2.在插入/删除的时候,也是默认在链尾操作。把插入的obj当成newLast,挂在oldLast的后面。另外还要先判断first是否为空,如果为空则first = obj。

    ——ArrayList 数组,随机访问,不同步(不安全)—动态数组 默认生成大小为0的数组,初次调用 add会更新为10,每次扩容是原来数组的一半 用的移位>>1
    ——Vector 数组,同步,线程安全,默认10,初始化的时候如果传入增长系数每次扩容按照系数,如果没有每次扩容是原来数组的一倍,不建议使用
    ——Stack 是Vector的实现类,同步,先进先出,栈存储

Collections.synchronizedList和vector相比

1.Synchronized有很好的的扩展性和兼容功能,可以转换所有的List子类安全
2.使用SynchroizedList的时候,进行遍历时要手动进行同步处理Synchronized类并没有都是用synchronized同步代码块(如listIterator),所以遍历需要手动同步。
3.synchronizedList 可以锁定指定的对象,用同步代码块,vector用的是同步方法。
1.同步方法使用synchronized修饰方法,在调用该方法前,需要获得内置锁(java每个对象都有一个内置锁),否则就处于阻塞状态
代码如: public synchronized void save(){//内容}
2.同步代码块使用synchronized(object){}进行修饰,在调用该代码块时,需要获得内置锁,否则就处于阻塞状态
代码如:
synchronized(object){
//内容
}

  • set接口:仅接收一次,不可重复,并做内部排序
    ——HashSet 使用hash表(数组)存储元素采用hash算法,底层用数组存储数据。
    不能保证迭代顺序,顺序不是恒久不变的,允许null元素,不允许重复元素。HashSet 是基于HashMap实现的,HashSet的元素都是放在hashMap的key上面,Value值是一个static final object 。存放链表的数组,默认初始化容量16,加载因子0.75。
    使用注意:HashSet的集合对象中自定义的类必须覆盖HashCode()和equals()方法才能保证集合中的元素不重复,如果数组中的元素要和加入元素有相同的hash值,才会用equals()判断两个对象内容是不是相同。
    ——LinkedHashSet 双向链表维护元素的插入顺序
    ——TreeSet 底层实现为二叉树,元素排好序,TreeSet是基于TreeMap的navigableSet实现,存在K中,V是常量,Java8 新增分割器spliterator()方法,适合排序
  • Map 键值对集合(双列集合)
    ——HashTable,同步,线程安全,是一个散列表,键值对存储,继承Dictionary,K 、V都不可为null,
    初始化因子:就是哈希表创建时的容量
    加载因子:是对哈希表容量自动增加之前可以达到多大的一个尺度 默认0.75
    ——HashMap 接口实现类 ,没有同步, 线程不安全- 默认初始化容量16,加载因子0.75 用map.entrySet().iterator 遍历效率高 容量16是为了服务于从key映射到index的hash算法 效率高。
    简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
    ——LinkedHashMap 双向链表和哈希表实现 通过双向链表的结构来维护节点顺序
    ——WeakHashMap 适合构建缓存系统 entry继承weakrefrence 做了分代缓存
    ——CurrentHashMap java5 主要是为了应对hashMap在并发情况下不安全而诞生的,做了分代缓存
    ——TreeMap 红黑树对所有的key进行排序
    ——IdentifyHashMap HashMap类判断k1和k2相等的条件是 (k1 == null?k2 == null:k1.equals(k2)) == true;dentifyHashMap判断的条件是(k1 == k2)
    ——Queue 队列 fcfs工作原理(first come first serve)

HashMap jdk 1.7 和1.8 的差异

背景:
1.7和1.8主要在处理哈希冲突和扩容问题上区别比较大。
jdk 1.7扩容的条件是 数组长度大于阈值且存在哈希冲突,由此我们可以想象,默认长度为16的情况下,数组最多可以存27个元素后才扩容,原因是在一个下标存储12个元素后(阈值为12),在剩下的15个下标各存一个元素,最多就可存27个元素,当然这种是很偶然的情况。不过也可以看到 jdk1.7 中,这个阈值的作用并不是特别的大,并不是超过阈值就一定会扩容。
1.8中,数组有两种情况会发生扩容,一种是超过阈值,一种是链表转为红黑树且数组元素小于64时,由此在jdk1.8中,默认长度为16情况下,要么元素一直放在同一下标,数组长度为9时就会扩容,要么超过阈值12时才会扩容。
总结:

  1. 出现哈希冲突时,1.7把数据存放在链表,1.8是先放在链表,链表长度超过8就转成红黑树
  2. 1.7扩容条件是数组长度大于阈值且存在哈希冲突,1.8扩容条件是数组长度大于阈值或链表转为红黑树且数组元素小于64时
  3. 链表过长,取数据的效率就很慢,红黑树插入比较慢,但取数据还是很快的。
    使用 hashmap 时,一开始最好指定下长度,毕竟扩容时,需要重新根据 key 计算数组下标,还是很影响效率的。

concurrentHashMap JDK1.7和1.8比较

其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock(重进入锁)+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。
1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS(Compare and Swap),即比较并替换+Synchronized保证线程安全。
3.保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性
4.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
5.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
6.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

HashTable和HashMap区别

HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。

HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator [i’nju:mə,reitə]迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
HashMap不能保证随着时间的推移Map中的元素次序是不变的。

HashSet和HashMap的区别

HashMap实现了Map接口 HashSet实现了Set接口
HashMap储存键值对 HashSet仅仅存储对象
HashMap使用put()方法将元素放入map中 HashSet使用add()方法将元素放入set中
HashMap中使用键对象来计算hashcode值 HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的性,如果两个对象不相等同的话,那么返回false
HashMap比较快,因为是使用唯一的键来获取对象 HashSet较HashMap来说比较慢

Java面试常问IO

2020年Android面试题三部曲——Java部分_第1张图片

概念

(1) 传统java.io包:对文件进行了抽象、通过输入流输出流进行IO 同步、阻塞 代码简单、直观
(2) java.net包: 网络通信同样是IO行为 同步、阻塞 网络API:Socket、ServerSocket、HttpURLConnection
(3) java.nio包:Java1.4中引入了NIO框架 同步、非阻塞 提供了 Channel、Selector、Buffer 等新的抽象
(4) java7的NIO2:引入了异步非阻塞IO方式 异步 IO 操作基于事件和回调机制—应用操作直接返回,而不会阻塞,当后台处理完成后,操作系统会通知相应线程进行后续工作。

NIO和IO的区别

  • IO面向流,从Stream中逐步读取数据,并且没有缓冲区。
    NIO面向面向缓冲区,数据整体操作更加高效。
  • IO是阻塞的,当前线程在没有数据可读时会出现阻塞。
    NIO是非阻塞的,通过Selector选择器选择合适的Channel进行数据操作。当一个Channel没有数据时,会切换到有效的Channel处理其他io,更高效。
    随着JDK1.4对IO进行了重构。NIO在速度上的优势并不存在了。
NIO真正优势体现在:

分散和聚集: 利用Scatter/Gather委托操作系统完成数据分散和聚集的工作
文件锁定功能:
网络异步IO: 非阻塞IO、IO多路复用(解决服务端多线程时的线程占用问题)
服务端多线程并发处理任务,即使使用线程池,高并发处理依然会因为上下文切换,导致性能问题。NIO是利用单线程轮询事件的机制,高效的去选择来请求连接的Channel仅提供服务。

IO流的两种分类方式

(1)字节流(基类是InputStream/OutputStream)、字符流(reader/writer)
(2)输入流、输出流
字符流是输出到缓冲区的。只有在调用close()方法关闭缓冲区时,信息才输出。要想字符流在未关闭时输出信息,则需要手动调用flush()方法
IO 工具类都实现了 Closeable 接口,因为需要进行资源的释放,cleaner 或 finalize 机制作为资源释放的最后把关,也是必要的。

Java.io包中六大类和接口

File、RandomAccessFile、InputStream、OutputStream、Reader、Writer
RandomAccessFile随机文件操作,一个独立的类,直接继承至Object.功能丰富,可以从文件的任意位置进行存取(输入输出)操作。
点击查看参考链接,更多IO详细

面向对象的六大原则

  • 单一职责原则, 一个类中应该是一组相关性很高的函数、数据的封装。
  • 开闭原则, 扩展是开放的 修改是封闭的
  • 里氏替换原则, 所有引用基类的地方必须能够透明的使用其子类。通俗的讲就是只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误和异常。
  • 依赖倒置原则, 指一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了
  • 接口隔离原则, 客户端不应该依赖它不需要的接口。类间的依赖关系应该建立在最小的接口上。
  • 迪米特原则,也就是最少知识原则,一个对象应对其他对象有最少的了解

线程和进程

线程的生命状态

  • 新建状态:
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  • 就绪状态:
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态:
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态:
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。
    在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
  1. 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
  2. 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
  3. 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。
    当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

Java 提供了三种创建线程的方法

  1. 通过实现 Runnable 接口;
  2. 通过继承 Thread 类本身;
  3. 通过 Callable 和 Future 创建线程。

进程和线程的区别:

  • 进程是应用程序,线程是一条执行路径。
  • 进程有独立的内存空间,崩溃不会影响其他程序,线程没有独立的空间,多个线程在同一个进程的空间,可能会影响其他线程
  • 一个进程中,至少有一个线程

run和start的区别

run没有开辟新的栈空间,没有新线程,都是主线程在执行
start开辟了新的栈空间,在新的栈空间启动run()方法

sleep和pield的区别

sleep 线程进入被阻塞的状态
yeild 线程转入暂停执行的状态

打断线程的终止方式

1、用标记,当终止线程时,会执行完run方法
2、stop()方法,不建议使用,会执行不到特定的代码
3、interrupt(),只能中断正在休眠的线程,通过抛异常的方法中断线程的终止。
InputStream inputStream=System.in;
int m=inputStream.read();
myThread2.interrupt();//通过外界输入打断

XML的三种原生解析方式(DOM,SAX,PULL)

DOM:内存消耗大 但是便于遍历.打开文档,将其转化为节点树,然后在其用循环的方式,遍历节点,一一查找.
SAX:速度快,战内存少.但是文件结构信息会丢失,采用的是流的处理方式.从起始标签开始一一往下逐个查找.起始标签与结尾标签作为标记来将一组数据存入一个集合中,像水流一样一直到最尾,然后最后返回集合,集合中就存下了所有的数据(这也应该就是所谓的流处理方式吧).
PULL:是Android内置,推荐的一种,相对来说有点类似SAX,也是从上往下,但是它还是以文档开始与结尾为条件,在其中间进行查找处理,然后分为不同标签开始逐一查找.

线程池相关问题

线程池 ThreadPoolExcutor -----FixedThreadPool SingleThreadPool CachedThreadPool ScheduledThreadPool
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory)
ThreadPoolExecutor有两个方法可以供我们执行,分别是submit()和execute(),我们先来看看这两个方法到底有什么差异。
在不需要线程执行返回结果值时,我们使用execute 方法。 而当我们需要返回值时,则使用submit方法,他会返回一个Future对象。Future不仅仅可以获得一个结果,他还可以被取消,我们可以通过调用future的cancel()方法,取消一个Future的执行。 比如我们加入了一个线程,但是在这过程中我们又想中断它,则可通过sumbit 来实现。
1.shutDown() 关闭线程池,不影响已经提交的任务
2.shutDownNow() 关闭线程池,并尝试去终止正在执行的线程
3.allowCoreThreadTimeOut(boolean value) 允许核心线程闲置超时时被回收
4.submit 一般情况下我们使用execute来提交任务,但是有时候可能也会用到submit,使用submit的好处是submit有返回值。
5.beforeExecute() - 任务执行前执行的方法
6.afterExecute() -任务执行结束后执行的方法
7.terminated() -线程池关闭后执行的方法
Runtime.getRuntime().availableProcessors();获取CPU的数量。

深入理解TCP/IP协议

tcp/Ip协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连接因特网,以及数据如何在它们之间进行传输。
协议之间的通信最终都是要转换为0和1的电信号,通过物理介质进行传输的,因此物理介质是网络通信的基石。

  1. 链路层:对电信号进行分组,形成具有意义的数据帧,然后以广播的形式通过物理介质发送给接收方
    以太网数据包:首部16字节,包含目标MAC地址、源mac地址和类型,网卡地址就是数据包的发送地址和接收地址也就是Mac地址,
    有了MAC地址后,以太网以广播的方式发送给该子网内的所有主机 ,主机对比自己的MAC地址选择接受。
    中部是数据, 尾部是4个字节 表示数据帧的校验序列,用于确定数据包在传输过程中是否损坏
  2. 网络层:定义网络地址,区分网段,子网内寻Mac地址,不同子网的数据进行路由
    IP协议、MAC地址只与生产厂家有关无法判断两台主机是否在同一个网络,因此制定了一套新的协议,使得我们能够区分两台主机是不是在同一网络,这就是网络协议叫做IP协议
    Ip地址与子网掩码进行按位与运算就可以得到网络地址了 如果两个IP地址在同一子网内,那么网络地址一定相同
    ARP协议、地址解析协议,是根据IP地址获取MAC地址的一个网络层协议
    路由协议 ARP协议的MAC寻址是在同一子网内 如果不在同一子网,以太网会将数据包转发给本子网的网关进行路由,最终会将数据发送到目标Ip所在的子网内,然后在ARP协议的MAC寻址 网关是子网与子网之间的桥梁
  3. 传输层:定义端口,标识应该程序身份,实现端口到端口的通信。
    背景是:一台主机好多程序 确定是哪个程序发出,哪个程序接收。
    UDP 面向数据报
    TCP 面向连接的、可靠的、基于字节流的通信协议
  • TCP三次握手过程
    主机A通过向主机B 发送一个含有同步序列号标志位的数据段(SYN)给主机B ,向主机B 请求建立连接,通过这个数据段,主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我。
    主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:我已经收到你的请求了,你可以传输数据了;你要用哪个序列号作为起始数据段来回应我。
    主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:“我已收到回复,我现在要开始传输实际数据了”。
    这样3次握手就完成了,主机A和主机B 就可以传输数据了。

  • TCP四次挥手过程
    当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求。
    主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1。
    由B 端再提出反方向的关闭请求,将FIN置1。
    主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束。

  1. 应用层:定义数据格式并按照对应的格式解读数据
    Accept表示客户端期望接收的数据格式
    ContentType是客户端发送的数据格式

类加载机制

首先,在代码被编译器编译后生成的二进制字节流(.class)文件;
然后,JVM把Class文件加载到内存,并进行验证、准备、解析、初始化;
最后,能够形成被JVM直接使用的Java类型的过程。
加载①. 通过类的全限定名来获取类的二进制字节流。
②. 将字节流中所有代表的静态存储结构转化为【方法区】的运行时数据结构。
③. 在内存Java堆中生成一个代表这个类的Java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。
连接 连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中
1)验证:确保被加载的类符合JVM规范和安全。验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,可以关闭以缩短虚拟机类加载的时间。
2)准备 静态变量在方法区分配内存 静态变量在分配内存后,附上初始值。为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
static final int a = 10; // 该静态常量a 会在【准备阶段】直接将10赋值。static int b = 11;// 该静态变量b 在【准备阶段】只会赋值初始值0,等到了【初始化】阶段会将真正的11赋值给静态变量b。

3)解析 把类中的符号引用转换为直接引用。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
4)初始化 初始化,为类的静态变量赋予正确的初始值。初始化阶段是执行类构造器()方法。
有且只有主动引用才会触发类初始化的过程
JVM初始化步骤
① 如果这个类还没有被加载和连接,则程序先加载并连接该类。(其实就是执行上面的类加载、连接两步骤)
② 如果的直接父类还没有被初始化,则先初始化其直接父类。
③ 如果这个类中有初始化语句,则系统会一次执行这些初始化语句。
类的初始化时机有且只有主动引用时才会触发类的初始化。
主动引用:创建类的实例。即通过new的方式,new一个对象;
调用类的静态变量(非final修饰的常量) 和静态方法。;
过反射对类进行调用。;
初始化某个类的子类,则父类也会被初始化;
Java虚拟机启动时,指定的main方法所在的类,需要被提前初始化。;
被动引用:
当访问一个类的静态变量时(该静态变量是父类所持有),只有真正声明这个变量的类才会初始化。;
通过数据定义引用类,不会触发类的初始化。;
final 常量不会触发类的初始化,因为编译阶段就存储在常量池中;
5)使用
类的正常使用。
6)卸载
类的卸载需要根据【该类对象不再被引用+GC回收 】来判断何时被卸载。
①由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。因为一直被引用着。
②由用户自定义的类加载器加载的类是可以被卸载

类加载器并不需要等到某个类被“首次主动使用”时才加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载。
如果预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器不会马上报告错误;类加载器必须在程序【首次主动使用】该类时才报告错误(LinkageError错误)。

类加载器

把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
比较两个类是否“相等“,只有在这两个类是由同一个类加载器加载的前提下才由意义;否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。
类加载器可以分为:

  • 启动类加载器(Bootstrap ClassLoader)是最顶层的类加载器,主要加载核心类库。
  • 扩展类加载器(Extension ClassLoader)
  • 应用程序类加载器(Application ClassLoader)Application ClassLoader是负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般情况下就是程序默认的类加载器。
  • 自定义类加载器(USer ClassLoader)般是继承ClassLoader,重写findClass方法。
    因为JVM自带的ClassLoader只会从本地文件系统加载标准的Java class文件,因此编写自定义类加载器可以做到:先自低向上检查是否加载过,再自上向下尝试加载

JAVA内存模型

2020年Android面试题三部曲——Java部分_第2张图片
2020年Android面试题三部曲——Java部分_第3张图片

  • 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器.个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
  • Java栈 - Java Stack
    生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表(方法参数、方法内部定义的局部变量 通过索引操作)、操作数栈(通过标准的栈操作——压栈和出栈—来访问的)、动态链接、方法出口(是不是正常退出)等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    动态链接:虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用
    如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析。
    如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。
  • 本地方法栈
    本地方法栈则是为虚拟机使用到的Native 方法服务
  • 方法区
    方法区用来存储类型的元数据信息,一个.class文件是类被java虚拟机使用之前的表现形式,一旦这个类要被使用,java虚拟机就会对其进行装载、连接(验证、准备、解析)和初始化,而装载后的结果就是由.class文件转变为方法区中的一段特定的数据结构
    2020年Android面试题三部曲——Java部分_第4张图片
  • 堆(垃圾回收)
    此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。(通过-Xmx和-Xms控制大小)
    现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。Eden : from : to = 8 :1 : 1
    老年代与新生代的大小比值,默认为2:1;新生代的初始值NewSize默认为1M

    (二) 新生代
    程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成。
    可通过-Xmn参数来指定新生代的大小;也可以通过-XX:SurvivorRation来调整Eden Space及SurvivorSpace的大小。
    可以通过参数-XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
    JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor 区域是空闲着的。
    新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
    Edan区 、From区 - Surivivor 0 、To 区 - Surivivor 1
    在未开始GC的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
    紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
    年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。
    这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。
    Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
    Minor GC、FullGC
    堆是GC收集垃圾的主要区域。GC 分为两种:Minor GC年轻、FullGC ( 或称为 Major GC )。
    Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
    当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,
    在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),
    则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,
    然后清理所使用过的 Eden 以及 Survivor 区域 ( 即from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,
    当对象的年龄达到某个值时 ( 默认是 15 岁),这些对象就会成为老年代。
    但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
    Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。
    Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。
    标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
    (三) 老生代
    用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:
    1、大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。
    2、大的数组对象,且数组中无引用外部对象。
    老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
    如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。(防止反射—应用的权限进行校验,保护用户的隐私目的)
Class类 代表类的实体,在运行的Java应用程序中表示类和接口
Field类 代表类的成员变量(成员变量也称为类的属性)
Method类 代表类的方法
Constructor类 代表类的构造方法

泛型

  • 定义:是将类型由原来的具体的类型参数化
  • Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
    泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

序列化

序列化是将对象状态转换为可保持或传输的格式的过程。

  • Serializable 手动设置serialVersionUID的好处是当前class如果改变了成员变量,比如增加或者删除之后,这个UID是不改变的,那么反序列化就不会失败;频繁造成GC
  • Parcelable android特有的序列化API,它的出现是为了解决Serializable在序列化的过程中消耗资源严重的问题,一般只获取内存数据的时候使用。
  • Externalizable:强制自定义序列化 必须实现writeExternal、readExternal方法。自动选择储存信息
    所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
    对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  • 注意:
  1. 如果想让某个变量不被序列化,使用transient修饰。
  2. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
  3. 反序列化时必须有序列化对象的class文件。
  4. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
  5. 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
  6. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  7. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。

几种常用的 sql 的关键字

–创建一个名为stonestory的数据库
create database stonestory

–创建一个名为roles的表
create table roles
(
roleId int primary key identity(1,1), --id号设为主键,从1开始递增量为1
roleName varchar(15), --名字 varchar(可变)
roleAge int, --年龄
roleSex char(2), --性别
)

–删除表(把表的结构本身和数据都删掉)
drop table roles

–插入语句
insert into roles values(‘林黛玉’,18,‘女’)

–查询语句
select * from roles --查询表中全部内容
select * from roles where roleAge<20 --查询表中年龄小于20的人物信息
select * from roles where roleAge between 18 and 28 --查询表中年龄在18到28之间的人物信息
select top 1 * from roles where roleId > 2 --查询表中id号大于2的人物信息(只显示第一条)

----查询id号除第一个的前两个人物信息(即第2,3位置的人物信息)
Select top 2 * from roles where roleId not in(select top 1 roleId from roles)

–更新语句(修改)
update roles set roleAge=19,roleSex=“男” where roleName=‘薛宝钗’ --将名为薛宝钗的人的年龄改为19

delete from roles --删除表中全部数据
delete from roles where roleName=‘贾政’ --删除表中名为贾政的人

必会排序算法

  • 冒泡
public class BubbleSoerOpt2 {
    public static void main(String[] args) {
        int[] list = {6,4,7,5,1,3,2};
        int len = list.length-1;
        int temp = 0; // 开辟一个临时空间, 存放交换的中间值   
        int tempPostion = 0;  // 记录最后一次交换的位置 第二次优化
        // 要遍历的次数
        for (int i = 0; i < list.length-1; i++) {
            int flag = 1; //设置一个标志位 第一次优化
            //依次的比较相邻两个数的大小,遍历一次后,把数组中第i小的数放在第i个位置上
            for (int j = 0; j < len; j++) {
                // 比较相邻的元素,如果前面的数小于后面的数,交换
                if (list[j] < list[j+1]) {
                    temp = list[j+1];
                    list[j+1] = list[j];
                    list[j] = temp;
                    flag = 0;  //发生交换,标志位置0
                    tempPostion = j;  //记录交换的位置
                }
             
            }
            len = tempPostion; //把最后一次交换的位置给len,来缩减内循环的次
            if (flag == 1) {//如果没有交换过元素,则已经有序
                return;
            }
                   
        }
    }
}
  • 选择
public class SelectSort {//基本选择排序
    public static void selectSort(int[] arr){
        for (int i = 0; i arr[j]){
                    int temp=arr[i];
                    arr[i]=arr[j];
                    arr[j]=temp;
 
                }
            }
        }
    }
    //选择排序的优化
    public static void selectSort2(int[] arr){
        for(int i = 0;iarr[j]){//如果最小元素大于比较的值,则将num值重新赋值,index赋值新的下标
                    num=arr[j];
                    index = j;
                }
            }
            if(index!=i){//如果index是i值,说明i下标的值是最小值,如果不是则说明不是,则交换
                int temp =arr[index];
                arr[index]=arr[i];
                arr[i]=temp;
            }
        }
    }
 
 
    //其实选择排序优化的本质就是减少了每次两元素相比之后相互交换的操作,而是直接记录下标用取得的元素去比较,直到找到最小的元素,然后再进行交换。
 
   
 public static void main(String[] args) {
        int[] arr={5,3,2,1,5,6,7};
       // selectSort(arr);
        selectSort2(arr);
        for (int i:arr
             ) {
            System.out.println(i);
            
        }
    }
 
}
  • 快速排序 分治法-挖坑发、指针交换法
    同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。快速排序的平均时间复杂度是 O(nlogn),最坏情况下的时间复杂度是 O(n^2)。
    更多详细
挖坑法
public class QuickSort {

public static void quickSort(int[] arr, int startIndex, int endIndex) {
// 递归结束条件:startIndex大等于endIndex的时候
if (startIndex >= endIndex) {
return;
}
// 得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 用分治法递归数列的两部分
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
// 坑的位置,初始等于pivot的位置
int index = startIndex;
//大循环在左右指针重合或者交错时结束
while ( right >= left ){
//right指针从右向左进行比较
while ( right >= left ) {
if (arr[right] < pivot) {
arr[left] = arr[right];
index = right;
left++;
break;
}
right--;
}
//left指针从左向右进行比较
while ( right >= left ) {
if (arr[left] > pivot) {
arr[right] = arr[left];
index = left;
right--;
break;
}
left++;
}
}
arr[index] = pivot;
return index;
}
public static void main(String[] args) {
int[] arr = new int[] {4,7,6,5,3,2,8,1};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
}
指针交换法 和挖坑法相比,指针交换法在partition方法中进行的元素交换次数更少。
public class QuickSort {

public static void quickSort(int[] arr, int startIndex, int endIndex) {
// 递归结束条件:startIndex大等于endIndex的时候
if (startIndex >= endIndex) {
return;
}
// 得到基准元素位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 根据基准元素,分成两部分递归排序
quickSort(arr, startIndex, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, endIndex);
}
private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while( left != right) {
//控制right指针比较并左移
while(left pivot){
right--;
}
//控制right指针比较并右移
while( left

二分查找
1.二分查找又称折半查找,它是一种效率较高的查找方法。
2.二分查找要求:(1)必须采用顺序存储结构 (2).必须按关键字大小有序排列
3.原理:将数组分为三部分,依次是中值(所谓的中值就是数组中间位置的那个值)前,中值,中值后
将要查找的值和数组的中值进行比较,若小于中值则在中值前 面找,若大于中值则在中值后面找,
等于中值时直接返回。然后依次是一个递归过程,将前半部分或者后半部分继续分解为三部分。
4.实现:
二分查找的实现用递归和循环两种方式

public class _00BinarySearch {
    public static void main(String[] args) {
        int[] arr = {6, 12, 33, 87, 90, 97, 108, 561};
        System.out.println("循环查找:" + binarySearch(arr, 87));
        System.out.println("递归查找:" + binSearch(arr, 0, arr.length - 1, 87));
    }

    //循环实现二分查找算法arr 已排好序的数组x 需要查找的数-1 无法查到数据
    public static int binarySearch(int[] srcArray, int des) {
        //定义初始最小、最大索引
        int low = 0;
        int high = srcArray.length - 1;
        //确保不会出现重复查找,越界
        while (low <= high) {
            //计算出中间索引值
            int middle = (high + low) >>> 1;//防止溢出
            if (des == srcArray[middle]) {
                return middle;
                //判断下限
            } else if (des < srcArray[middle]) {
                high = middle - 1;
                //判断上限
            } else {
                low = middle + 1;
            }
        }
        //若没有,则返回-1
        return -1;
    }

    /**
     * 二分查找递归实现。
     * @param srcArray 有序数组
     * @param start    数组低地址下标
     * @param end      数组高地址下标
     * @param key      查找元素
     * @return 查找元素不存在返回-1
     */
    public static int binSearch(int srcArray[], int start, int end, int key) {
//        int mid = (end - start) / 2 + start;
        int mid = (end - start) >>> 1;
        if (srcArray[mid] == key) {
            return mid;
        }
        if (start >= end) {
            return -1;
        } else if (key > srcArray[mid]) {
            return binSearch(srcArray, mid + 1, end, key);
        } else if (key < srcArray[mid]) {
            return binSearch(srcArray, start, mid - 1, key);
        }
        return -1;
    }
}

你可能感兴趣的:(Android面试集锦,android,java,面试)