Java基础知识

Posted Roni

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java基础知识相关的知识,希望对你有一定的参考价值。

1. Java程序初始化的顺序是怎么样的

 在Java语言,当实例化对象时,对象所在类的所有成员变量首先要进行实例化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建对象。

初始化一般遵循3个原则:

  • 静态对象(变量)优先于非静态对象(变量)初始化,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次;
  • 父类优先于子类进行初始化;
  • 按照成员变量的定义顺序进行初始化。 即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造函数)被调用之前先初始化;

加载顺序

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

2. Java和C++的区别

  • Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
  • Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
  • Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
  • Java 支持自动垃圾回收,而 C++ 需要手动回收。(C++11 中引入智能指针,使用引用计数法垃圾回收)
  • Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
  • Java 不支持操作符重载,虽然可以对两个 String 对象支持加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
  • Java 内置了线程的支持,而 C++ 需要依靠第三方库。
  • Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
  • Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。

参考资料:

3. 反射

 首先看一个在知乎上的优秀回答吧:

 反射是什么呢?当我们的程序在运行时,需要动态的加载一些类,这些类可能之前用不到,所以不用加载到 JVM,而是在运行时根据需要才加载,这样的好处对于服务器来说不言而喻。

举个例子我们的项目底层有时是用 mysql,有时用 oracle,需要动态地根据实际情况加载驱动类,这个时候反射就有用了,假设 com.java.dbtest.myqlConnection,com.java.dbtest.oracleConnection 这两个类我们要用,这时候我们的程序就写得比较动态化,通过 Class tc = Class.forName("com.java.dbtest.TestConnection"); 通过类的全类名让 JVM 在服务器中找到并加载这个类,而如果是 Oracle 则传入的参数就变成另一个了。这时候就可以看到反射的好处了,这个动态性就体现出 Java 的特性了!

  举多个例子,大家如果接触过 spring,会发现当你配置各种各样的 bean 时,是以配置文件的形式配置的,你需要用到哪些 bean 就配哪些,spring 容器就会根据你的需求去动态加载,你的程序就能健壮地运行。

 

什么是反射

  反射 (Reflection) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。通过 Class 获取 class 信息称之为反射(Reflection)

简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。

程序中一般的对象的类型都是在编译器确定下来的,而Java反射机制可以动态地创建对象并调用其属性,这样的对象地类型在编译器是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译器是未知的。

反射的核心是JVM在运行时才动态或调用方法/访问属性,它不需要事先(写代码的时候或编译器)知道运行对象是谁。

Java反射框架主要提供以下功能:

1.在运行时判断任意一个对象所属的类

2.在运行时构造任意一个类的对象

3.在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法)

4.在运行时调用任意一个对象的方法

  重点:是运行时而不是编译时

 

主要用途

  很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。

当我们在使用 IDE (如Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。

 

反射最重要的用途就是开发各种通用框架

  很多框架(比如 Spring )都是配置化的(比如通过 XML 文件配置 JavaBean,Action 之类的),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。

  对与框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点,不过了解一下框架的底层机制有助于丰富自己的编程思想,也是很有益的。

 获得class对象

1.调用运行时类的本身的.class属性

Class claszz1 = Person.class;
sout(claszz1.getName());

 

2.通过运行时类的对象获取getClass()

Person p = new Person();
Class clazz3 = p.getClass();
System.out.println(clazz3.getName());

 

3.使用Class类的forName静态方法

public static Class<?> forName(String className)
// 在JDBC开发中常用此方法加载数据库驱动:
Class.forName(driver);

 

4.了解通过类的加载器ClassLoader()

ClassLoader classLoader = this.getClass().getClassLoader();
Class clazz5 = classLoader.loadClass(className);
System.out.println(clazz5.getName());

4. 注解

什么是注解

Annontation 是 Java5 开始引入的新特征,中文名称叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation 像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。

Java注解时附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到说明、配置的功能。注解不会也不能影响代码时实际逻辑,仅仅起到辅助性的作用。包含在Java.langannotation包中。

简单来说:注解就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,比起执行相对应的处理。

为什么要用注解

传统的方式,我们时通过配置文件.xml来告诉类如何运行的。

有了注解技术以后,我们就可以通过注解告诉类如何运行。

比如:我们以前编写servlet的时候,需要在web.xml文件配置具体的信息。我们使用了注解以后,可以直接在servlet源代码上,增加注解...servlet就被配置到Tomcat上了。也就是说,注解可以给类、方法上注入信息。

这样也是非常直观的,并且servlet规范时推崇这种配置的。

基本Annotation

 在Java.lang包下存着的5各基本Annotation,重点掌握前三个。

@Override 重写注解

  • 如果我们使用IDE重写父类的方法,我们就可以看见它了。
  • @Override是告诉编译器要检查该方法是实现父类的,可以帮我们避免一些低级的错误。
  • 比如,我们在实现 equals() 方法的时候,把 euqals() 打错了,那么编译器就会发现该方法并不是实现父类的,与注解 @Override 冲突,于是就会给予错误。

@Deprecated 过时注解

  • 该注解也非常常见,Java 在设计的时候,可能觉得某些方法设计得不好,为了兼容以前的程序,是不能直接把它抛弃的,于是就设置它为过时。
  • Date对象中的 toLocalString() 就被设置成过时了
  • 当我们在程序中调用它的时候,在 IDE 上会出现一条横杠,说明该方法是过时的。
@Deprecated
public String toLocaleString() {
    DateFormat formatter = DateFormat.getDateTimeInstance();
    return formatter.format(this);
}

 

@SuppressWarnings 抑制编译器警告注解

  • 该注解在我们写程序的时候并不是很常见,我们可以用它来让编译器不给予我们警告
  • 当我们在使用集合的时候,如果没有指定泛型,那么会提示安全检查的警告
  • 如果我们在类上添加了@SuppressWarnings这个注解,那么编译器就不会给予我们警告了
  1. @SafeVarargs Java 7“堆污染”警告

    • 什么是堆污染呢??当把一个不是泛型的集合赋值给一个带泛型的集合的时候,这种情况就很容易发生堆污染。
    • 这个注解也是用来抑制编译器警告的注解,用的地方并不多。
  2. @FunctionalInterface 用来指定该接口是函数式接口

    • 用该注解显示指定该接口是一个函数式接口。

自定义注解类编写规则

  1. Annotation 型定义为 @interface, 所有的 Annotation 会自动继承 java.lang.Annotation 这一接口,并且不能再去继承别的类或是接口.
  2. 参数成员只能用 public 或默认(default)这两个访问权修饰
  3. 参数成员只能用基本类型 byte,short,char,int,long,float,double,boolean 八种基本数据类型和 String、Enum、Class、annotations 等数据类型,以及这一些类型的数组
  4. 要获取类方法和字段的注解信息,必须通过 Java 的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法
  5. 注解也可以没有定义成员, 不过这样注解就没啥用了 PS:自定义注解需要使用到元注解
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 水果名称注解
 */
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitName {
    String value() default "";
}

 

5. 泛型

 通俗解释

通俗地讲,泛型就是操作类型地占位符,即假设占位符为T,那么此次声明地数据结构操作的数据类型为T类型。

假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型地数组进行排序,该如何实现》答案是可以用Java泛型。

使用Java泛型地概念,我们可以写一个泛型方法来对一个对象数组排序。然后调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

泛型方法

你可以写一个泛型方法,该方法在调用时可以接受不同类型地参数,根据传递给泛型方法地参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法地规则

1.所有泛型方法声明都有一个类型参数声明部分(用尖括号分隔),该类型参数声明不放在方法返回类型之前(在下面栗子中的<E>)

2.每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于知道一个泛型类型名称地标识符。

3.类型灿书能被用来声明返回值类型,并可以作为泛型方法得到的实际参数书类型的占位符。

4.泛型方法的声明和其他方法一样。注意类型参数只能代表引用类型,不能是原始类型(像int,double,char等)

{
   // 泛型方法 printArray                         
   public static < E > void printArray( E[] inputArray )
   {
      // 输出数组元素            
         for ( E element : inputArray ){        
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }
 
    public static void main( String args[] )
    {
        // 创建不同类型数组: Integer, Double 和 Character
        Integer[] intArray = { 1, 2, 3, 4, 5 };
        Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
        Character[] charArray = { \'H\', \'E\', \'L\', \'L\', \'O\' };
 
        System.out.println( "整型数组元素为:" );
        printArray( intArray  ); // 传递一个整型数组
 
        System.out.println( "\\n双精度型数组元素为:" );
        printArray( doubleArray ); // 传递一个双精度型数组
 
        System.out.println( "\\n字符型数组元素为:" );
        printArray( charArray ); // 传递一个字符型数组
    } 
}

泛型类

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

public class Box<T> {
    private T t;
    public void add(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        Box<String> stringBox = new Box<String>();

        integerBox.add(new Integer(10));
        stringBox.add(new String("菜鸟教程"));

        System.out.printf("整型值为 :%d\\n\\n", integerBox.get());
        System.out.printf("字符串为 :%s\\n", stringBox.get());
    }
}

类型通配符

  1. 类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是 List<String>List<Integer> 等所有 List<具体类型实参> 的父类。

  2. 类型通配符上限通过形如 List 来定义,如此定义就是通配符泛型值接受 Number 及其下层子类类型。

  3. 类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其三层父类类型,如 Objec 类型的实例。

6. 字节与字符的区别

理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:

类型概念描述举例
字符 人们使用的记号,抽象意义上的一个符号。 \'1\', \'中\', \'a\', \'$\', \'¥\', ……
字节 计算机中存储数据的单元,一个 8 位的二进制数,是一个很具体的存储空间。 0x01, 0x45, 0xFA, ……
ANSI 字符串 在内存中,如果“字符”是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为 ANSI 字符串或者多字节字符串。 "中文123" (占7字节)
UNICODE 字符串 在内存中,如果“字符”是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串。 L"中文123" (占10字节)

 

字节与字符区别

它们完全不是一个位面的概念,所以两者之间没有“区别”这个说法。不同编码里,字符和字节的对应关系不同:

类型概念描述
ASCII 一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。一个二进制数字序列,在计算机中作为一个数字单元,一般为 8 位二进制数,换算为十进制。最小值 0,最大值 255。
UTF-8 一个英文字符等于一个字节,一个中文(含繁体)等于三个字节
Unicode 一个英文等于两个字节,一个中文(含繁体)等于两个字节。符号:英文标点占一个字节,中文标点占两个字节。举例:英文句号“.”占 1 个字节的大小,中文句号“。”占 2 个字节的大小。
UTF-16 一个英文字母字符或一个汉字字符存储都需要 2 个字节(Unicode扩展区的一些汉字存储需要4个字节)
UTF-32 世界上任何字符的存储都需要 4 个字节

 

7. 有哪些访问修饰符

Java 面向对象的基本思想之一是封装细节并且公开接口。Java 语言采用访问控制修饰符来控制类及类的方法和变量的访问权限,从而向使用者暴露接口,但隐藏实现细节。访问控制分为四种级别:

 

修饰符当前类同 包子 类其他包
public
protected ×
default × ×
private × × ×

 

  • 类的成员不写访问修饰时默认为 default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。
  • 受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。
  • Java 中,外部类的修饰符只能是 public 或默认,类的成员(包括内部类)的修饰符可以是以上四种。

8. 深拷贝与浅拷贝

  • 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所拷贝的对象,而不复制它所引用的对象。
  • 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

10. 字符串常量池

  Java 中字符串对象创建有两种形式,一种为字面量形式,如 String str = "abc";,另一种就是使用 new 这种标准的构造对象的方法,如 String str = new String("abc");,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式。然而这两种实现其实存在着一些性能和内存占用的差别。这一切都是源于 JVM 为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池。

工作原理

  当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。

public class Test {
    public static void main(String[] args) {

        String s1 = "abc";
        String s2 = "abc";

        // 以上两个局部变量都存在了常量池中
        System.out.println(s1 == s2); // true


        // new出来的对象不会放到常量池中,内存地址是不同的
        String s3 = new String();
        String s4 = new String();

        /**
         * 字符串的比较不可以使用双等号,这样会比较内存地址
         * 字符串比较应当用equals,可见String重写了equals
         */
        System.out.println(s3 == s4); // false
        System.out.println(s3.equals(s4)); // true
    }
}

 

11. 解释型语言与编译型语言的区别

  我们使用工具编写的字母加符号的代码,是我们能看懂的高级语言,计算机无法直接理解,计算机需要先对我们编写的代码翻译成计算机语言,才能执行我们编写的程序。

  将高级语言翻译成计算机语言有编译,解释两种方式。两种方式只是翻译的时间不同。

1. 编译型语言

  编译型语言写得程序在执行之前,需要借助一个程序,将高级语言编写的程序翻译成计算机能懂的机器语言,然后,这个机器语言就能直接执行了,也就是我们常见的(exe文件)。

2. 解释型语言

  解释型语言的程序不需要编译,节省了一道工序,不过解释型的语言在运行的时候需要翻译,每个语句都是执行的时候才翻译,对比编译型语言,效率比较低。通俗来讲,就是借助一个程序,且这个程序能试图理解编写的代码,然后按照编写的代码中的要求执行。

3. 脚本语言

  脚本语言也是一种解释型语言,又被称为扩建的语言,或者动态语言不需要编译,可以直接使用,由解释器来负责解释。

脚本语言一般都是以文本形式存在,类似于一种命令。

4. 通俗理解编译型语言和解释型语言

  同行讨论编译型语言和解释型语言的时候,这么说过,编译型语言相当于做一桌子菜再吃,解释型语言就是吃火锅。解释型的语言执行效率低,类似火锅需要一边煮一边吃。

 

  • 重载:重载发生在同一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载。
  • 重写:重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。根据不同的子类对象确定调用的那个方法。

 

4. 面向对象开发的六个基本原则,在项目中用过哪些原则

六个基本原则(参考《设计模式之禅》)

  • 单一职责
    • (Single Responsibility Principle 简称 SRP):一个类应该仅有一个引起它变化的原因。在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。
  • 里氏替换
    • (Liskov Substitution Principle 简称 LSP):任何时候子类型能够替换掉它们的父类型。子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。
  • 依赖倒置
    • (Dependence Inversion Principle 简称 DIP):要依赖于抽象,不要依赖于具体类。要做到依赖倒置,应该做到:①高层模块不应该依赖底层模块,二者都应该依赖于抽象;②抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
  • 接口隔离
    • (Interface Segregation Principle 简称 ISP):不应该强迫客户依赖于他们不用的方法 。接口要小而专,绝不能大而全。臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。
  • 最少知识原则
    • (Least Knowledge Principle 简称 LKP):只和你的朋友谈话。迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。
  • 开闭原则(Open Closed Principle 简称 OCP):软件实体应当对扩展开放,对修改关闭。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱。
  • 其他原则

    • 合成聚和复用:优先使用聚合或合成关系复用代码
    • 面向接口编程
    • 优先使用组合,而非继承
    • 一个类需要的数据应该隐藏在类的内部
    • 类之间应该零耦合,或者只有传导耦合,换句话说,类之间要么没关系,要么只使用另一个类的接口提供的操作
    • 在水平方向上尽可能统一地分布系统功能
  • 项目中用到的原则

    • 单一职责、开放封闭、合成聚合复用(最简单的例子就是String类)、接口隔离

 

5. 内部类有哪些

可以将一个类的定义放在另一个类的定义内部,这就是内部类。

在 Java 中内部类主要分为成员内部类、局部内部类、匿名内部类、静态内部类

public class OuterClass {
    private String str;
   
    public void outerDisplay(){
        System.out.println("outerClass...");
    }
    
    public class InnerClass{
        public void innerDisplay(){
            str = "chenssy..."; //使用外围内的属性
            System.out.println(str);
            outerDisplay();  //使用外围内的方法
        }
    }
    
    // 推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时
    public InnerClass getInnerClass(){
        return new InnerClass();
    }
    
    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        OuterClass.InnerClass inner = outer.getInnerClass();
        inner.innerDisplay();
    }
}
--------------------
chenssy...
outerClass...

在成员内部类中要注意两点:

  • 成员内部类中不能存在任何 static 的变量和方法;

  • 成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。

----------

 

 

6. 组合、继承和代理的区别

定义

  • 组合:在新类中 new 另外一个类的对象,以添加该对象的特性。
  • 继承:从基类继承得到子类,获得父类的特性。
  • 代理:在代理类中创建某功能的类,调用类的一些方法以获得该类的部分特性。

使用场合

组合:各部件之间没有什么关系,只需要组合即可。例如组装电脑,需要new CPU(),new RAM(),new Disk()

public class Computer {
    public Computer() {
        CPU cpu=new CPU();
        RAM ram=new RAM();
        Disk disk=new Disk();
    }
}
class CPU{    }
class RAM{    }
class Disk{    }

继承:子类需要具有父类的功能,各子类之间有所差异。例如 Shape 类作为父类,子类有 Rectangle,CirCle,Triangle……代码不写了,大家都经常用。