Java基本概念
问题及答案来源自《Java程序员面试笔试宝典》第四章 Java基础知识 4.1基本概念
1、Java语言有何优势?
- 纯面向对象的语言
- 具有平台无关性,可移植性好
- 提供了很多内置的类库
- 提供了对web应用开发的支持
- 具有很好的安全性和健壮性
- 除去了C++中难以理解和容易混淆的特性(头文件、指针、多重继承等)
2、Java与C/C++有何区别?
Java和C++的相同点:
Java和C++都是面向对象语言,都使用了面向对象思想(例如封装、继承、多态等)
由于面向对象有许多非常好的特性(继承、组合等),因此二者都有很好的重用性
Java和C++的不同点:
- Java为解释性语言,而C/C++为编译型语言(Java执行速度比C/C++慢,但Java能跨平台执行而C/C++不能)
- Java代码运行过程:源代码通过Java编译器编译成字节码(.class),然后由JVM解释执行
- C/C++代码运行过程:源代码通过编译和链接后生成可执行的二进制代码
- Java为纯面向对象语言,所有代码要在类中实现,而C++兼具面向过程和面向对象编程的特点
- Java没有C/C++中的指针
- Java没有C++中的多重继承,但是Java引入了接口的概念,可以通过实现多个接口来实现与C++中多重继承一样的目的
- 在C++中需要开发人员管理内存分配(申请和释放),而Java中提供了垃圾回收器来实现垃圾的自动回收
- C++支持运算符重载,而Java中不支持运算符重载
- Java具有平台无关性(每种数据类型都分配固定长度),而C/C++的同一个数据类型在不同平台上会分配不同的字节数
3、为什么需要public static void main(String[] args)这个方法?
public static void main(String[] args)为Java程序的入口方法,JVM在运行程序时,会首先查找main方法
其中public是权限修饰符,表明任何类和对象都可以访问这个方法,static表明main方法是一个静态方法
静态方法是存储在静态存储区的,只要类被加载后,就可以使用该方法而不需要通过实例化对象来访问,可以
直接通过类名.main()直接访问
JVM在启动时是按照上述方法的签名(public、static void String[])来查找方法的入口地址,若能找到就执行
注意:
- 入口方法main,返回值必须为void,并有static和public关键字修饰,不能用abstract关键字修饰
- 只有与文件名相同的用public修饰的类中的main方法才能作为整个程序的入口方法
4、如何实现在main方法执行前输出"Hello,world"?
直接看下面的代码:
1 public class testDemo { 2 static{ 3 System.out.println("test hello"); 4 } 5 6 public static void main(String[] args) { 7 System.out.println("Hello World!"); 8 } 9 }
输出结果:
test hello
Hello World!
5、Java程序初始化的顺序是怎么样的?
Java程序的初始化一般遵循以下三个原则:
- 静态对象(变量)优先于非静态对象(变量)初始化(静态只初始化一次,非静态可能会初始化多次)
- 父类优先于子类进行初始化
- 按照成员变量的定义顺序进行初始化
Java程序初始化工作可以在不同代码块中完成(静态代码块、非静态代码块、构造函数),它们执行的顺序如下:
父类静态变量 =》父类静态代码块 =》子类静态变量 =》子类静态代码块 =》父类非静态变量 =》父类非静态代码块 =》父类构造函数 =》子类非静态变量 =》子类非静态代码块 =》子类构造函数
示例代码:
1 // 初始化顺序 2 class Base{ 3 static{ 4 // 静态代码块 5 System.out.println("Base static block"); 6 } 7 { 8 // 非静态代码块 9 System.out.println("Base block"); 10 } 11 public Base() { 12 // 构造函数 13 System.out.println("Base constructor"); 14 } 15 } 16 17 public class Demo extends Base{ 18 static{ 19 // 静态代码块 20 System.out.println("Derived static block"); 21 } 22 { 23 // 非静态代码块 24 System.out.println("Derived block"); 25 } 26 public Demo() { 27 // 构造函数 28 System.out.println("Derived constructor"); 29 } 30 public static void main(String[] args) { 31 new Demo(); 32 } 33 }
输出结果:
Base static block
Derived static block
Base block
Base constructor
Derived block
Derived constructor
6、Java中的作用域有哪些?
在Java中,作用域是由花括号的位置决定的,它决定了其定义的变量名的可见性生命周期
在Java中,变量的类型分为三种:成员变量、静态变量和局部变量,其作用域如下:
- 类的成员变量的作用范围与类的实例化对象的作用范围相同,当类被实例化时,成员变量就会在内存中分配空间并进行初始化,直到这个被实例化对象的生命周期结束时,成员变量的生命周期才结束
- 类的静态变量不依赖于特定实例,而是被所有实例共享,也就是说只要一个类被加载,JVM就会给类的静态变量分配存储空间
- 局部变量的作用域的可见性为它所在的花括号内
另外成员(成员变量和成员方法)有四种作用域:
- public:表明成员对所有类和对象都是可见的,所有类和对象都可以直接访问
- protected:表明成员对该类自身,与它在同一个包中的类,在其他包中的该类的子类都可见
- private:表明成员是私有的,只有当前类有访问权限,除此之外的其他类和对象都没有访问权限
- default:不指明修饰符时,表明成员只有自己和与其位于同一包内的类可见
注意:这些修饰符只能修饰成员变量和成员方法,不能修饰局部变量,private和protected不能用来修饰类(只有public、abstract或final能修饰类)
7、一个Java文件中是否可以定义多个类?
当然可以,见下面的代码:
1 class Base{ 2 public void print(){ 3 System.out.println("Base"); 4 } 5 } 6 7 public class Derived extends Base{ 8 public static void main(String[] args){ 9 Base c = new Derived(); 10 c.print(); 11 } 12 }
一个Java文件可以定义多个类,但是最多只能有一个类被public修饰,并且这个类的类名与文件名相同,这个public类中的main函数就是程序的入口
若文件中没有public类,则文件名随便是一个类的名字即可,需要注意的是当用javac指令编译Java文件时,每一个类会生成一个单独的class文件
8、什么是构造函数?
构造函数是一种特殊的函数,用来在对象实例化时初始化对象的成员变量
Java中的构造函数具有如下特定:
- 构造函数与类同名并且没有返回值(返回值也不能为void)
- 当不提供构造函数时编译器在把代码编译成字节码的过程中会提供一个默认没有参数的构造函数
- 构造函数总是伴随着new操作一起被调用(系统调用)
- 构造函数的主要作用是完成对象的初始化工作
- 构造函数不能被继承,构造函数可以重载(每个类声明多个构造函数)
- 子类可以通过super关键字来显示调用父类的构造函数
9、为什么Java有些接口没有任何方法?
什么是接口:
- 接口是抽象方法定义的集合(也可以定义一些常量在接口中),是一种特殊的抽象类
- 接口中只包含方法的定义,没有方法的实现,接口中所有方法都是抽象的
- 接口中成员的作用域都是public,接口中常量值默认使用public static final修饰
- 由于一个类可以实现多个接口,因此通常采用实现多个接口的方式来间接达到多重继承的目的
在Java中,有些接口内部没有声明任何方法(实现这些接口的类不用重写任何方法),这些没有任何方法声明的接口又叫标识接口,
标识接口对实现它的类没有任何语义上的要求,仅仅起标识的作用,用来表明实现它的类属于一个特定的类型
Java类库中已经存在的标识接口有Cloneable和Serializable等
10、Java中的clone方法有什么作用?
在实际编程中,经常会遇到从某个已知的对象A创建出另外一个与A具有相同状态的对象B,并且对A修改不会影响到B的状态
但是在Java中不能通过简单的赋值操作完成这个过程(赋值仅仅是传递引用,只是浅拷贝),而Java中提供了clone方法
来满足这个需求
Java中所有的类都默认继承自Object类,而Object类中提供了一个clone()方法,这个方法的作用
是返回一个Object对象的复制,这个复制方法返回的是一个新的对象而不是一个引用。以下是使用clone()方法的步骤:
- 实现clone的类首先需要继承Cloneable接口(Cloneable接口实质是一个标识接口,没有任何的接口方法)
- 在类中重写Object类中的clone()方法
- 在clone()方法中调用super.clone()。无论clone类继承结构是什么,super.clone()会直接或间接java.lang.Object类中的clone()方法。
- 把浅复制的引用指向原型对象新的克隆体
代码如下:
1 class Obj implements Cloneable { 2 private int i = 0; 3 public int getI() { 4 return i; 5 } 6 public void setI(int i) { 7 this.i = i; 8 } 9 public void changeI() { 10 this.i = 1; 11 } 12 public Object clone() { //重写clone()方法 13 Object o = null; 14 try { 15 o = (Obj)super.clone(); 16 }catch(CloneNotSupportedException e) { 17 e.printStackTrace(); 18 } 19 return o; 20 } 21 } 22 23 public class LianXi { 24 public static void main(String[] args) { 25 Obj a = new Obj(); 26 Obj b = (Obj)a.clone(); 27 b.changeI(); 28 System.out.println("a:"+a.getI()); // 0 29 System.out.println("b:"+b.getI()); // 1 30 } 31 }
当类中只有一些基本的数据类型时,采用上述方法就可以了,但是当类包含一些对象时,就需要使用到深复制了,
实现方法是在对对象调用clone方法完成复制后接着对对象中的非基本类型的属性也调用clone方法完成深复制
代码如下:
1 class obj2 implements Cloneable{ 2 private Date birth = new Date(); 3 public Date getBirth(){ 4 return birth; 5 } 6 public void setBirth(Date birth){ 7 this.birth = birth; 8 } 9 10 public void changeDate(){ 11 this.birth.setMonth(6); 12 } 13 public Object clone(){ 14 obj2 o = null; 15 try{ 16 o = (obj2)super.clone(); 17 } catch(CloneNotSupportedException e){ 18 e.printStackTrace(); 19 } 20 // 实现深复制 21 o.birth = (Date)this.getBirth().clone(); 22 return o; 23 } 24 } 25 26 public class cloneDemo{ 27 public static void main(String[] args) { 28 obj2 a = new obj2(); 29 obj2 b = (obj2)a.clone(); 30 a.changeDate(); 31 System.out.println("a=" + a.getBirth()); 32 System.out.println("b=" + b.getBirth()); 33 } 34 }
总结 - 在编程时如何选择复制方式
首先,检查类有无非基本类型(即为对象的数据成员) ,若没有,返回super.clone()即可
若有,确保类中包含的所有非基本类型的成员变量都实现了深复制
1 Object o = super.clone(); // 先执行浅复制 2 // 对每一个对象attr执行下列语句: 3 o.attr = this.getAttr().clone(); 4 // 最后返回o
引申 - 浅复制和深复制有什么区别?
浅复制:被复制对象的所有变量都含有与原来对象相同的值,而被复制对象中的对象的引用任然指向原对象
深复制:引用其他对象的变量将指向被复制的新对象,而不是原来的引用指向的对象
假如定义如下的类:
1 class Test{ 2 public int i; 3 public StringBuffer s; 4 }
则对于这个类深复制和浅复制的区别如下:
11、什么是反射机制
什么是Java中的反射机制:
在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,
都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用
对象的方法的功能就称为java语言的反射机制。通俗点讲,通过反射,该类对我们来说是完全透明的,想要获取任何东西都可以
反射提供的功能:
- 得到一个对象所属的类
- 获取一个类的所有成员变量和方法
- 在运行时创建对象
- 在运行时调用对象的方法
在运行时动态的创建类的对象:
1 class Base{ 2 public void f(){ 3 System.out.println("Base"); 4 } 5 } 6 7 class Sub extends Base{ 8 public void f(){ 9 System.out.println("Sub"); 10 } 11 } 12 13 public class Demo{ 14 public static void main(String[] args) { 15 try{ 16 // 使用反射机制加载类 17 Class c = Class.forName("Sub"); 18 Base b = (Base)c.newInstance(); 19 b.f(); 20 } catch(Exception e){ 21 e.printStackTrace(); 22 } 23 } 24 }
程序运行结果为:
Sub
在类加载的时候,JVM会创建一个class对象
class对象是可以说是反射中最常用的,获取class对象的方式的主要有三种:
- 根据类名:类名.class
- 根据对象:对象.getClass()
- 根据全限定类名:Class.forName(全限定类名)
拓展 - Java创建对象的方式有几种? 四种:
- 用new创建对象
- 用反射机制创建对象
- 通过clone方法创建对象
- 通过反序列化方法创建对象
12、package有什么作用?
package的中文意思是包,是对Java源文件进行分类管理的组织
包的作用:
- 提供多层命名空间,解决命名冲突
- 对类按功能进行分类,使项目的组织更加清晰
导入包:import xxx;
全限定类名:包名.类名(可以不同导入包),后期反射中会用到全限定类名
13、如何实现类似C语言中函数指针的功能?
C语言中的函数指针:
在C语言中的函数指针,其重要作用就是实现回调函数
回调函数:
所谓回调函数,就是指函数先在某处定义,而后它将在某个需要的位置被调用。回调函数一般用于
截获消息、获取系统信息或处理异步事件
函数指针一般作为函数的参数来使用,开发人员在使用时可以根据自己的需求传递自定义的函数来实现指定的功能
比如在排序算法中,通过传递一个函数指针来决定两个数的先后顺序,从而决定算法是按升序还是按降序排序
C语言实现如下:
1 #include<stdio.h> 2 3 int Cmp1(int a,int b) 4 { 5 if(a>b) 6 return 1; 7 else if(a<b) 8 return -1; 9 else 10 return 0; 11 } 12 13 int Cmp2(int a,int b) 14 { 15 if(a>b) 16 return -1; 17 else if(a<b) 18 return 1; 19 else 20 return 0; 21 } 22 23 void insertSort(int a[],int n,int(*p)(int,int)) //指向函数的指针做函数参数 24 { 25 int i; 26 for(i=1;i<n;i++) 27 { 28 int temp = a[i]; 29 int j = i-1; 30 while(j>=0&&(*p)(a[j],temp)==1) 31 { 32 a[j+1] = a[j]; 33 j--; 34 } 35 a[j+1] = temp; 36 } 37 } 38 39 int main() 40 { 41 int array[] = {7,3,19,40,4,7,1}; 42 printf("从小到大排序\n"); 43 insertSort(array,7,Cmp1); //将函数入口地址传给指向函数指针 44 for(int i=0;i<7;i++) 45 { 46 printf("%d ",array[i]); 47 } 48 printf("\n从大到小排序\n"); 49 insertSort(array,7,Cmp2); //将函数入口地址传给指向函数指针 50 for(int i=0;i<7;i++) 51 { 52 printf("%d ",array[i]); 53 } 54 return 0; 55 }
在Java中可以利用接口和类实现类似与函数指针的功能,先定义一个接口,然后在接口中声明要调用的方法,
接着实现这个接口(升序和降序两种),最后把这个实现类的一个对象作为参数传递给调用程序,调用程序
通过这个参数来调用指定的函数,从而实现回调函数的功能,示例如下:
1 // 接口 2 public interface IntCompare { 3 public int cmp(int a,int b); 4 }
1 // 实现1 升序 2 class Cmp1 implements IntCompare 3 { 4 public int cmp(int a,int b) 5 { 6 if(a>b) 7 return 1; 8 else if(a<b) 9 return -1; 10 else 11 return 0; 12 } 13 } 14 15 // 实现2 降序 16 class Cmp2 implements IntCompare 17 { 18 public int cmp(int a,int b) 19 { 20 if(a>b) 21 return -1; 22 else if(a<b) 23 return 1; 24 else 25 return 0; 26 } 27 } 28 29 public class testDemo 30 { 31 public static void main(String[] args) 32 { 33 int[] array1 = {7,3,19,40,4,7,1}; 34 insertSort(array1,new Cmp1()); 35 System.out.println("升序排列"); 36 for(int i=0;i<array1.length;i++) 37 { 38 System.out.print(array1[i]+" "); 39 } 40 41 System.out.println(); 42 int[] array2 = {7,3,19,40,4,7,1}; 43 insertSort(array2,new Cmp2()); 44 System.out.println("降序排列"); 45 for(int i =0;i<array2.length;i++) 46 { 47 System.out.print(array2[i]+" "); 48 } 49 50 } 51 52 public static void insertSort(int[] a,IntCompare cmp) 53 { 54 if(a!=null) 55 { 56 57 for(int i=1;i<a.length;i++) 58 { 59 int temp = a[i],j=i; 60 if(cmp.cmp(a[j-1], temp)==1) 61 { 62 while(j>=1&&cmp.cmp(a[j-1],temp)==1) 63 { 64 a[j] = a[j-1]; 65 j--; 66 } 67 } 68 a[j] = temp; 69 } 70 for(int i=1;i<a.length;i++) 71 { 72 int temp = a[i]; 73 int j = i-1; 74 while(j>=0&&cmp.cmp(a[j], temp)==1) 75 { 76 a[j+1] = a[j]; 77 j--; 78 } 79 a[j+1] = temp; 80 } 81 } 82 } 83 }