Java基本语法算然很容易掌握,但想彻底熟悉Java语法和原理却需要很多时间和精力,而读书是最直接的学习方式,《Think in Java》和《Effective Java》两本书是初学和进阶都要读的书,扎实了Java基础,熟悉了Java高级编程,才能在解决其它问题时游刃有余。结合实际开发中遇到的各种问题以及进阶学习中遇到的瓶颈,感觉有必要再来重新学习一下Java了。
static 修饰 类:
static修饰类时,是指修饰内部类,也就是所说的静态内部类,普通外部类是不允许用static修饰的。
非静态内部类可以直接访问外部类的成员变量和成员方法,即使是private的。实例化非静态内部类时要先实例化一个外部类对象outerObject,通过这个外部类的对象来实例化:NonStaticInner nonStaticInnerObject = outerObject.new NonStaticInner();非静态内部类将隐式地持有外部类对象的引用,如果你用不到这个引用,那这个引用将只会白白消耗内存并影响垃圾收集器GC的回收。非静态内部类可以看成是外部类的一个组成部分,外部类对象被销毁了,对应的内部类对象也会被销毁。如果用static修饰非静态内部类成员变量必须是static final的并初始化,不能用static修饰非静态内部类成员方法。
静态内部类就像是普通的外部类一样,对象的创建生死完全由自己决定,也就意味着静态内部类的对象不会依赖外部类对象,垃圾收集器可以很好的对这个对象进行回收。静态内部类只能直接访问外部类的静态成员变量和静态成员方法,如果要访问外部类中的实例变量和实例方法必须要通过外部类对象实例去访问。实际上,静态内部类就像其它地顶层类一样使用,只不过为了封装方便将它写在另一个顶层类里面而已。实例化时像顶层类一样实例化:OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass(); 在Android中线程中的Handler类建议是静态内部类,因为如果该Handler是非静态内部类而且Handler处理消息时碰到一个延时很长时间的消息,此时内存紧张GC需要回收该线程对应的Activity实例,由于Handler持有该Activity实例的引用导致GC没法回收,从而导致内存泄漏(memory leak)。所以利用静态内部类和弱引用解决这个问题:
private final MainActivityHandler mainActivityHandler = new MainActivityHandler(this); private static class MainActivityHandler extends Handler { private final WeakReference<MainActivity> mActivity; public MainActivityHandler(MainActivity activity) { mActivity = new WeakReference<MainActivity>(activity); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); final MainActivity activity = mActivity.get(); if (activity != null) { //处理消息 } } }
Android API中也大量使用了静态内部类,比如常见的ViewHolder模式、Builder模式,一个很重要的原因就是避免内部类持有外部类的引用。既然提到了内存泄漏,也说一下面试时经常问到的“内存泄漏和内存溢出的区别”:
内存泄漏(memory leak)是指成功的申请了内存资源,但使用完之后没有释放,始终占着这块内存资源不放,导致别人没法用这块内存资源,自己也没办法用,偶然的内存泄漏对程序员和用户都是察觉不到的,但是频繁的内存泄漏会导致系统可用的内存越来越小、越来越小,最终导致内存不够用,也就是内存溢出。
内存溢出(out of memory)是指应用所需要的内存超过了系统(也就是JVM)能分配给应用的内存,从而导致应用没法正常工作,最直接的后果就是应用程序崩溃,严重了还会导致系统安全问题。内存溢出无法根本解决,只能尽可能避免,比如说在应用程序申请内存之前对可用内存进行检查、增大分配给应用的内存、优化程序代码、减少应用中的大数据复杂操作等。
static 修饰 成员变量:
静态变量属于类,被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。
static 修饰 成员方法:
static方法不依赖对象而存在,所以可以直接通过类名调用,工具类中的方法经常声明为static的。方法内部不能访问类的非静态成员变量和非静态成员方法,因为非静态成员变量和非静态成员方法都必须依赖具体的对象才能够被调用。
static 修饰 代码块:
用来形成静态代码块以优化程序性能。因为被static包裹的静态代码块只会在类加载的时候仅执行一次。可以利用static代码块完成一些初始化工作,如:
public class Outer { private static String date; static{ date = String.valueOf(2015-1950); } }
在看QQ源码时也经常看到会利用static代码块对静态成员变量进行初始化。
static不能用来修饰形参和局部变量。既然提到了类的初始化过程,笔试题中经常出现对初始化过程的理解:
普通类初始化:
static成员变量及static代码块初始化→普通成员变量初始化→构造器
派生类初始化:
父类static成员变量及static代码块初始化→子类static成员变量及static代码块初始化→父类普通成员变量初始化→父类构造器→子类普通成员变量初始化→子类构造器。
final 修饰 类:
fianl类一个终结类,不能被继承,final类中的成员方法默认加了final修饰。通常功能封装类或其它安全类声明为final以避免被重写破坏。
final 修饰 成员变量:
必须在定义处或构造器中初始化,一旦初始化就不能再改变。但对于引用类型的变量,引用虽然不能变,但所引用对象的属性可以变。
final 修饰 成员方法:
final方法禁止被子类覆写,同时也表明该方法允许编译器内联调用。
final 修饰 方法的形参:
基本数据类型:对实参变量无影响。 引用类型:由于实参和形参引用的是一个对象,因此用fianl修饰形参可以防止所引用的对象被破坏
----------------------------------------------------------------------------------------------------------------------------------
Java快排算法:
快排算法的核心是分治,将一个大问题分解为几个小问题,然后将小问题的解合并,就求出了大问题的解。快排算法就是递归地将一个无序数组排序分成两个子数组排序,分而治之,前一个数组是小的,后一个数组是大的,排序好一合并就行了。目前我接触到的最常见的算法实现有三种:普通单基准快排、三路快排(3-way sort)、双基准快排(dual-pivot sort)。
我之前学快排是看《算法导论》上的算法自己实现的:
public class QuickSort { public static void quickSort(int[] a, int left, int right) { if(left >= right) { return; } int m = partition(a, left, right); quickSort(a, left, m - 1); quickSort(a, m + 1, right); } /* * Partitioning,一次分割过程 * * left right * +-------------------------------------------------+ * | last| * +-------------------------------------------------+ * ^ ^ * | | * i j * * 规则: * * 以最右边的元素(last)作为基准(pivot) * * (left, i] <= 基准 * (i, j) > 基准 * * 遍历结束后j指向right,即基准。交换i+1和j所指元素,以将基准放入合适位置 */ public static int partition(int[] a, int left, int right) { int last = a[right]; int i = left - 1; for(int j = left; j <= right - 1; j++) { //j依次从左向右遍历(不包括最右边的基准元素),找出所有小于等于基准的元素并前移 //即:如果j处元素小于等于基准,i右移并交换i和j if(a[j] <= last) { i++; if(i != j) { a[i] = a[i] ^ a[j]; a[j] = a[i] ^ a[j]; a[i] = a[i] ^ a[j]; } } } //for循环结束时j == right if((i+1) != right) { a[i+1] = a[i+1] ^ a[right]; a[right] = a[i+1] ^ a[right]; a[i+1] = a[i+1] ^ a[right]; } return i+1; } public static void main(String[] args) { int[] a = {5, 1, 7, 9, 5, 2}; QuickSort.quickSort(a, 0, a.length - 1); for(int i = 0; i < a.length; i++) { System.out.print(a[i]+","); } } }
int型变量互换利用位运算实现,如果学过ACM对这种写法就不会感到奇怪了,不过我看到有人不是利用交换来实现,而是利用直接覆盖来实现,还形象地叫做“填坑法”,实现是这样的,以最左边的元素作为基准:
public class QuickSort { public static void quickSort(int[] a, int left, int right) { if(left >= right) { return; } int m = partition(a, left, right); quickSort(a, left, m - 1); quickSort(a, m + 1, right); } /* * Partitioning,一次分割过程 * * left right * +-------------------------------------------------+ * |pivot | * +-------------------------------------------------+ * ^ ^ * | | * i j * * 规则: * * 以最左边的元素作为基准(pivot) * * [left, i) < 基准 * [j, right] >= 基准 * * 遍历结束后i == j,将基准放入i/j所指元素,以将基准放入合适位置 */ public static int partition(int[] a, int left, int right) { int pivot = a[left]; int i = left; int j = right; while(i < j) { while(i < j && a[j] >= pivot) { j--; } a[i] = a[j]; while(i < j && a[i] < pivot) { i++; } a[j] = a[i]; } //循环结束后i == j a[i] = pivot; return i; } public static void main(String[] args) { int[] a = {5, 1, 7, 9, 5, 2}; QuickSort.quickSort(a, 0, a.length - 1); for(int i = 0; i < a.length; i++) { System.out.print(a[i]+","); } } }就是i从左边找出大的放到右边,j从右边找小的放在左边,把基准存起来,那基准的位置就可以随便填坑覆盖了,嗯,填坑,是挺形象的。
虽然快排的平均复杂度是O(n log(n)),但如果恰好基准就是最小的那个或者大量重复数据,复杂度就会退化成O(n²),所以真正要用到的排序肯定不能这样写,JDK7里面将Josh Bloch(JDK中的java.math、assertions、Collections集合框架都是Joshua J. Bloch的杰作,《Effective Java》的作者)、Vladimir Yaroslavskiy和Jon Bentley( ProgrammingPearls(《编程珠玑》)的作者)三位大师写的双基准快排算法写到Arrays.sort里,当然里面也结合了插入排序,具体源码和算法实现就不说了,有兴趣可以ctrl左键去看源码。
堆排序、快速排序、希尔排序、直接选择排序是不稳定的排序算法,其它排序算法都是稳定的。稳定不是指排序不同数组时执行效率一样就稳定,而是指数组中相同的元素不能因为排序而导致前后相对位置发生变化,这才是排序算法的稳定性。