码迷,mamicode.com
首页 > 编程语言 > 详细

面试-java知识基础

时间:2016-05-13 12:25:43      阅读:218      评论:0      收藏:0      [点我收藏+]

标签:

下面是java的一些面试点,主要是基础知识。

1.  最常见的运行时异常    

  java运行时异常是可能在java虚拟机正常工作时抛出的异常。    

  java提供了两种异常机制。一种是运行时异常(RuntimeExepction),一种是检查式异常(checked execption)。

  检查式异常:我们经常遇到的IO异常及sql异常就属于检查式异常。对于这种异常,java编译器要求我们必须对出现的这些异常进行catch 所以 面对这种异常不管我们是否愿意,只能自己去写一堆catch来捕捉这些异常。

  运行时异常:我们可以不处理。当出现这样的异常时,总是由虚拟机接管。比如:我们从来没有人去处理过NullPointerException异常,它就是运行时异常,并且这种异常还是最常见的异常之一。

  RuntimeExecption在java.lang包下,

  ClassCastException(类转换异常)

  IndexOutOfBoundsException(数组越界)

  NullPointerException(空指针)

  ArrayStoreException(数据存储异常,操作数组时类型不一致)

  还有IO操作的BufferOverflowException异常

 

2.  String类是否可以被继承

  不能,final修饰的。

 

3. 一个".java"源文件中是否可以包括多个类(不是内部类)

  可以,但是一个文件中只能有一个public类,并且此public类必须与文件名相同。

4. 面向对象的编程方法具有四个基本特征:

抽象
抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。

 继承

类的重用。一个新类可以从现有的类中派生,这个过程称为类继承。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要,提高代码的重用行。

 封装

   封装是把数据和过程(对数据的操作)包围起来,隐藏内部的实现细节,对数据的访问只能通过已定义的方法 。

 多态

   多态性是指允许不同类的对象对同一消息作出响应。

 

5.spring的aop编程(aop Aspect Oriented Programming 面向切面编程

AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP/OOP区分

AOP、OOP在字面上虽然非常类似,但却是面向不同领域的两种设计思想。

OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。

上面的陈述可能过于理论化,举个简单的例子,对于“雇员”这样一个业务实体进行封装,自然是OOP/OOD的任务,我们可以为其建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。而用AOP设计思想对“雇员”进行封装将无从谈起。

同样,对于“权限检查”这一动作片断进行划分,则是AOP的目标领域。而通过OOD/OOP对一个动作进行封装,则有点不伦不类。

换而言之,OOD/OOP面向名词领域,AOP面向动词领域。

 

IOC和AOP的区别

IoC就是Inversion of Control,控制反转。在Java开发中,IoC意味着将你设计好的类交给系统去控制,而不是在你的类内部控制。这称为控制反转。

将对象的创建和获取提取到外部。由外部容器提供需要的组件。

AOP 关注与主要的东西,也可以说让你只关注与业务,其他的东西就让AOP帮你完成。比如事务管理、持久化、资源池、系统统一的认证、权限管理等。允许通过分离应用的业务逻辑与系统级服务进行内聚性的开发。

 

6. 快速和归并算法

 快速排序:

  设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。

  一趟快速排序的算法是:

  1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;

  2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];

  3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;

  4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;

  5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成  的时候,此时令循环结束)。

  排序演示

  假设用户输入了如下数组:

下标

0

1

2

3

4

5

数据

6

2

7

3

8

9

  创建变量i=0(指向第一个数据), j=5(指向最后一个数据), k=6(赋值为第一个数据的值)。

  我们要把所有比k小的数移动到k的左面,所以我们可以开始寻找比6小的数,从j开始,从右往左找,不断递减变量j的值,我们找到第一个下标3的数据比6小,于是把数据3移到下标0的位置,把下标0的数据6移到下标3,完成第一次比较:

下标

0

1

2

3

4

5

数据

3

2

7

6

8

9

  i=0 j=3 k=6

  接着,开始第二次比较,这次要变成找比k大的了,而且要从前往后找了。递加变量i,发现下标2的数据是第一个比k大的,于是用下标2的数据7和j指向的下标3的数据的6做交换,数据状态变成下表:

下标

0

1

2

3

4

5

数据

3

2

6

7

8

9

  i=2 j=3 k=6

  称上面两次比较为一个循环。

  接着,再递减变量j,不断重复进行上面的循环比较。

  在本例中,我们进行一次循环,就发现i和j“碰头”了:他们都指向了下标2。于是,第一遍比较结束。得到结果如下,凡是k(=6)左边的数都比它小,凡是k右边的数都比它大:

下标

0

1

2

3

4

5

数据

3

2

6

7

8

9

  如果i和j没有碰头的话,就递加i找大的,还没有,就再递减j找小的,如此反复,不断循环。注意判断和寻找是同时进行的。

  然后,对k两边的数据,再分组分别进行上述的过程,直到不能再分组为止。

  注意:第一遍快速排序不会直接得到最终结果,只会把比k大和比k小的数分到k的两边。为了得到最后结果,需要再次对下标2两边的数组分别执行此步骤,然后再分解数组,直到数组不能再分解为止(只有一个数据),才能得到正确结果。

  代码:

public class QuickSort {
    private static int par(int[]arr,int left,int right){
            int key=arr[left];

            while(left < right){
                //从最右边开始找比key小的数
                while(left<right && arr[right]>=key)
                    right--;
                
                //把找到的数移到左边
                arr[left]=arr[right];
                
                //从最左边开始找比key大的数
                while(left<right && arr[left]<=key)
                    left++;
                
                //把找到的数移到右边
                arr[right]=arr[left];
            }
            
            //最后,left等于right,将key至于中间,这样,key之前的数,比key小,key之后的数,比key大
            arr[left]=key;
            return left;
    }
    
    private static void quickSort(int[]arr,int left,int right) {
        if(arr==null || arr.length==0 || left>=right){
            return ;
        }
        int pos=par(arr,left,right);
        quickSort(arr,left,pos-1);
        quickSort(arr,left+1,right);
    }
    
    public static void main(String[]args) {
        int[] arr={22,11,23,32,45,32,21,33,67,12};
        quickSort(arr,0,arr.length-1);
        for(int k:arr)
            System.out.print(k+" ");
    }
}

 

 归并排序

  建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

归 并过程为:比较a[i]和a[j]的大小,若a[i]≤a[j],则将第一个有序表中的元素a[i]复制到r[k]中,并令i和k分别加上1;否则将第二 个有序表中的元素a[j]复制到r[k]中,并令j和k分别加上1,如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中 从下标k到下标t的单元。归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左 区间和右区间用一次归并操作合并成有序的区间[s,t]。

  归并操作

  归并操作(merge),也叫归并算法,指的是将两个顺序序列合并成一个顺序序列的方法。

  如 设有数列{6,202,100,301,38,8,1}

  初始状态:6,202,100,301,38,8,1

  第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3;

  第二次归并后:{6,100,202,301},{1,8,38},比较次数:4;

  第三次归并后:{1,6,8,38,100,202,301},比较次数:4;

  总的比较次数为:3+4+4=11,;

  逆序数为14;

 

  算法描述

  归并操作的工作原理如下:

  第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

  第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置

  第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

  重复步骤3直到某一指针超出序列尾

  将另一序列剩下的所有元素直接复制到合并序列尾

       技术分享

 代码:

public class MergeSort {
    
    public static void mergeSort(int[] arr) {
    	if(arr==null || arr.length<=1){
    		return;
    	}
        mSort(arr, 0, arr.length-1);
    }

    public static void mSort(int[] arr, int left, int right) {
        if(left >= right)
            return ;
        int mid = (left + right) / 2;
        
        mSort(arr, left, mid); 		//递归排序左边
        mSort(arr, mid+1, right); 	//递归排序右边
        merge(arr, left, mid, right); //合并
    }
    
    /**
     * 合并两个有序数组
     * @param arr 待合并数组
     * @param left 左指针
     * @param mid 中间指针
     * @param right 右指针
     */
    public static void merge(int[] arr, int left, int mid, int right) {
        //[left, mid] [mid+1, right]
        int[] temp = new int[right - left + 1]; //中间数组
        
        int i = left;
        int j = mid + 1;
        int k = 0;
        while(i <= mid && j <= right) {
            if(arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            }
            else {
                temp[k++] = arr[j++];
            }
        }
        
        while(i <= mid) {
            temp[k++] = arr[i++];
        }
        
        while(j <= right) {
            temp[k++] = arr[j++];
        }
        
        for(int p=0; p<temp.length; p++) {
            arr[left + p] = temp[p];
        }
        
    }
    
    public static void main(String[] args) {
    	int[] arr={1,98,33,31,35,2,55,78,44,3};
    	mergeSort(arr);
    	  for(int k:arr)
        	System.out.print(k+" ");
    }
}

 

7. 洗牌算法

   所谓洗牌算法,就是给你一个1到n的序列,让你随机打乱,保证每个数出现在任意一个位置的概率相同,也就是说在n!个的排列中,每一个排列出现的概率相同。

我们考虑,当一个数被选之后,我们是没有必要在下一次随机的时候再考虑它的,因此,我们每次只从可选的数的集合中进行随机,也就不用考虑是否会碰到已经选过的数了,这样子直接将算法的复杂度降了一个数量级。

我们考虑,当一个数被选之后,我们是没有必要在下一次随机的时候再考虑它的,因此,我们每次只从可选的数的集合中进行随机,也就不用考虑是否会碰到已经选过的数了,这样子直接将算法的复杂度降了一个数量级。

  

void MySwap(int &x, int &y)
{
    int temp = x;
    x = y;
    y = temp;
}

void Shuffle(int n)
{
    for(int i=n-1; i>=1; i--)
    {
        MySwap(num[i], num[rand()%(i+1)]);
    }
}

 

8. Java序列化

把对象转换为字节序列的过程—序列化。

把字节序列恢复为对象的过程—反序列化。

对象的序列化主要有两种用途:
  1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2) 在网络上传送对象的字节序列。

 

JDK类库中的序列化API

  java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
  java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
  只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。
  对象序列化包括如下步骤:
  1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  2) 通过对象输出流的writeObject()方法写对象。

  对象反序列化的步骤如下:
  1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
  2) 通过对象输入流的readObject()方法读取对象。

 

9. String和StringBuffer的区别

  StringBuffer对象的内容可以修改;而String对象一旦产生后就不可以被修改,重新赋值后生成的新对象。

10. b/s和c/s的区别

硬件环境不同:

C/S 一般建立在专用的网络上, 小范围里的网络环境, 局域网之间再通过专门服务器提供连接和数据交换服务.  B/S 建立在广域网之上的, 不必是专门的网络硬件环境,例与电话上网, 租用设备. 信息自己管理. 有比C/S更强的适应范围, 一般只要有操作系统和浏览器就行

 

对安全要求不同 :

  C/S 一般面向相对固定的用户群, 对信息安全的控制能力很强. 一般高度机密的信息系统采用C/S 结构适宜. 可以通过B/S发布部分可公开信息. 

  B/S 建立在广域网之上, 对安全的控制能力相对弱, 面向是不可知的用户群.  

 

对程序架构不同:

  C/S 程序可以更加注重流程, 可以对权限多层次校验, 对系统运行速度可以较少考虑. 

  B/S 对安全以及访问速度的多重的考虑, 建立在需要更加优化的基础之上. 比C/S有更高的要求 B/S结构的程序架构是发展的趋势, 从MS的.Net系列的BizTalk 2000 Exchange 2000等, 全面支持网络的构件搭建的系统. SUN 和IBM推的JavaBean 构件技术等,使 B/S更加成熟. 

 

软件重用不同:

C/S 程序可以不可避免的整体性考虑, 构件的重用性不如在B/S要求下的构件的重用性好. 

  B/S 对的多重结构,要求构件相对独立的功能. 能够相对较好的重用.就入买来的餐桌可以再利用,而不是做在墙上的石头桌子

 

系统维护不同 :

  系统维护是软件生存周期中,开销大, -------重要 

  C/S 程序由于整体性, 必须整体考察, 处理出现的问题以及系统升级. 升级难. 可能是再做一个全新的系统 

  B/S 构件组成,方面构件个别的更换,实现系统的无缝升级. 系统维护开销减到最小.用户从网上自己下载安装就可以实现升级.

 

处理问题不同:

  C/S 程序可以处理用户面固定, 并且在相同区域, 安全要求高需求, 与操作系统相关. 应该都是相同的系统 

  B/S 建立在广域网上, 面向不同的用户群, 分散地域, 这是C/S无法作到的. 与操作系统平台关系最小.  

 

用户接口不同 

  C/S 多是建立的Window平台上,表现方法有限,对程序员普遍要求较高 

  B/S 建立在浏览器上, 有更加丰富和生动的表现方式与用户交流. 并且大部分难度减低,减低开发成本.

 

11.private可以修饰方法里的变量吗?

   不可以,private咳哟用来修饰类变量。方法内的变量作用域是从这个变量声明直到方法体结束,如果再使用private修饰的话两者就会冲突,所以不能在方法体内声明一个变量为private,而且在方法里 声明变量为private也没什么意义,方法内声明的变量的作用域本来就在方法体内,自然也不会被其它方法所访问。

 

12.当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?

可以进入其他非synchronized的方法,synchronized的方法不可以的!
Java中的每个对象都有一个锁(lock)或者叫做监视器(monitor),当访问某个对象的synchronized方法时,表示的将该对象上锁,此时其他任何线程都无法再去访问该synchronized方法了,直到之前的那个线程执行方法完毕后(或者是抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该synchronized方法。
如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。

13.对象的上转型,子类重写了基类的方法A,那么上转型对象操作的这个方法A,是基类的方法A?还是子类的方法A?

  对象的上转型,操作的是子类的方法A。上转型对象只能操作基类的变量,子类继承的方法和子类重写的方法,不能使用子类新增的变量和方法。

 14 for循环里可以有boolean的表达式吗?

  可以。for( boolean1; boolean2&&条件表达式;boolean3)是正确的。

 15switch()的条件语句类型:

  byte、short、char、int、enum

  在jdk1.5上,Byte、Short、Character、Integer也支持。

 16 TreeMap是有序的map结构,HashMap不是,HashMap也是线程不稳定的。

 17hibernate操作数据库的运行过程。

  1  读取并解析配置文件

        Configuration conf = new Configuration().configure();

  2 读取并解析映射信息,创建SessionFactory

       SessionFactory sf  = conf.buildSessionFactoty();

  3 打开Session

      Session session = sf.openSession;//sf.getCurrentSession();

  4 开始一个事务(增删改操作必须,查询操作可选)

      Transaction tx = session.beginTransaction();

  5 数据库操作

     session.sava();

  6 提交事务(回滚事务)

     tx.commit();(tx.rollback();)

  7 关闭session

     session.close();

 18 定义内部类,外部怎么调用?

  一:static声明的内部类

  假设定义了一个类Outer,在Outer里定义了一个内部类Inner。

class Outer{        // 定义外部类  
    private static String info = "hello world" ;    // 定义外部类的私有属性  
    static class Inner{                // 使用static定义内部类为外部类  
        public void print(){                // 定义内部类的方法  
            System.out.println(info) ;      // 直接访问外部类的私有属性  
        }  
    };  
    public void fun(){                      // 定义外部类的方法  
        new Inner().print() ;               // 通过内部类的实例化对象调用方法  
    }  
}

  那么编译后会生成Outer.class和Outer$Inner.class。

  通过Outer.Inner in=new Outer.Inner(); 访问内部类。

  

  二:不使用statc声明一个内部类

 

class Outer{        // 定义外部类  
    private static String info = "hello world" ;    // 定义外部类的私有属性  
    class Inner{         // 使用static定义内部类为外部类  
        public void print(){                // 定义内部类的方法  
            System.out.println(info) ;      // 直接访问外部类的私有属性  
        }  
    };  
    public void fun(){                      // 定义外部类的方法  
        new Inner().print() ;               // 通过内部类的实例化对象调用方法  
    }  
}

  通过 Outer o=new Outer();  Outer.Inner in=o.new Inner();访问内部类。

  注意点:内部类不能含有static变量和方法,因为成员内部类的创建依赖于外部类对象。只有创建了外部类对象,才能创建内部类对象。

 

19 hibernate的inverse用法和含义

  Inverse是hibernate双向关系中的基本概念。inverse的真正作用就是指定由哪一方来维护之间的关联关系。当一方中指定了“inverse=true”,那么那一方就有责任负责之间的关联关系。

在set节点里设置。

 

20 get和post的区别

  ①浏览器和服务器会对用get方式提交的数据进行限制,服务器会对用post方式提交的数据进行限制。

  ②post的请求数据放置在http请求包中。

  ③get的参数个数理论上没有限制,但是浏览器和服务器会对用get方式提交的数据进行限制。

另外,get方式的提交的url采用ASCII编码。

 

21 看看下面的运行结果

  1. public static void main(String args[]){ {

     String a; 

     System.out.println("a="+a);

    }

 2. public class InnerClassDemo03{  
        static String a;
        public static void main(String args[]){ 
              System.out.println("a="+a);
        }  
    }

  结果:第一个编译会有问题,第二个运行输出a=null。

  分析:第一个很好理解,使用前没进行初始化,所以报错。第二个的a其实是成员变量,编译器有个默认初始化,初始化为null,所以有运行结果。

 

22 List、Set、Map的区别

  java集合(可以存储和操作数目不固定的一组数据,只能存放引用类型的数据,不能存放基本类型的数据,位于java.util包中)主要分为三种类型,set、list、map。

  List:可自动扩展的数组。

  Set:没有重复的数组。

23 java文件经编译后,会生成字节码(class)文件。

24 Spring里集成的hibernate的DAO层继承的哪个类?hibernateDAOSupport

25 部署java应用的服务器有哪些?和ajax有关的框架有哪些?

  部署java应用:Tomcat、Resin、Jboss、Weblogic

  和ajax有关的框架:JQuery、DWR、Dojo、ExtJs、Mootools

26 通知垃圾回收的方法

  System.gc()方法强制垃圾回收

27 抽象类和接口的区别:

① abstract class 在 Java 语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface,实现多重继承。接口还有标识(里面没有任何方法,如Remote接口)和数据共享(里面的变量全是常量)的作用。abstract class和interface所反映出的设计理念不同。其实abstract class表示的是"is-a"关系,interface表示的是"has-a"关系

② 接口是公开的,里面不能有私有的方法或变量,是用于让别人使用的,而抽象类是可以有私有方法或私有变量的。

③ 在abstract class 中可以有abstract的成员方法,也可以有非abstarct的成员方法,而在interface中所有的成员方法默认都是public abstract 类型的

④ 接口中定义的变量默认是public static final 型,且必须给其初值,所以实现类中不能重新定义,也不能改变其值。抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以在子类中重新赋值。

⑤ 实现接口的一定要实现接口里定义的所有方法,而实现抽象类可以有选择地重写需要用到的方法,一般的应用里,最顶级的是接口,然后是抽象类实现接口,最后才到具体类实现。抽象类中可以有非抽象方法。接口中则不能有实现方法。

 

28 transient使用小结

1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

 

29 构造初始化顺序

对象的初始化顺序:(1)类加载之后,按从上到下(从父类到子类)执行被static修饰的语句;(2)当static语句执行完之后,再执行main方法;(3)如果有语句new了自身的对象,将从上到下执行构造代码块构造器(两者可以说绑定在一起)。

在Java中,子类的构造过程中必须调用其父类的构造函数,是因为有继承关系存在时,子类要把父类的内容继承下来。但如果父类有多个构造函数时,该如何选择调用呢?第 一个规则:子类的构造过程中,必须调用其父类的构造方法。一个类,如果我们不写构造方法,那么编译器会帮我们加上一个默认的构造方法(就是没有参数的构造 方法),但是如果你自己写了构造方法,那么编译器就不会给你添加了,所以有时候当你new一个子类对象的时候,肯定调用了子类的构造方法,但是如果在子类构造方法中我们并没有显示的调用基类的构造方法,如:super();  这样就会调用父类没有参数的构造方法。    

第二个规则:如果子类的构造方法中既没有显示的调用基类构造方法,而基类中又没有无参的构造方法,则编译出错,所以,通常我们需要显示的:super(参数列表),来调用父类有参数的构造函数,此时无参的构造函数就不会被调用。

总之,一句话:子类没有显示调用父类构造函数,不管子类构造函数是否带参数都默认调用父类无参的构造函数,若父类没有则编译出错。

面试-java知识基础

标签:

原文地址:http://www.cnblogs.com/lan-writenbook/p/5473345.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!