数据的传递
1. 实参的数目、数据类型和次序必须和所调用的方法声明的形式参数列表匹配。
2. return 语句终止方法的运行并指定要返回的数据。
3. Java中进行方法调用中传递参数时,遵循值传递的原则(传递的都是数据的副本):
4. 基本类型传递的是该数据值的copy值。
5. 引用类型传递的是该对象引用的copy值,但指向的是同一个对象。
递归的调用:
自己调用自己”,一个使用递归技术的方法将会直接或者间接的调用自己。
递归结构包括两个部分:
1.定义递归头。解答:什么时候不调用自身方法。如果没有头,将陷入死循环,也就是递归的结束条件。
2.递归体。解答:什么时候需要调用自身方法。
任何能用递归解决的问题也能使用迭代解决。当递归方法可以更加自然地反映问题,并且易于理解和调试,并且不强调效率问题时,可以采用递归;
在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。
面向对象的内存:
java虚拟机的内存可以分为三个区域: 栈stack ,堆heap,方法区method area
栈stack:
1, 栈描述的是方法执行的内存模型,每个方法被调用都会创建一个栈帧(存储局部变量,操作数,方法出口等)
2. JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数,局部变量等)
3. 栈的存储特性私有,不能实现线程间共享
4.栈的存储特性是“先进后出,后进先出”
5. 栈是由系统自动分配,速度快, 栈是一个连续的内存空间
堆:
1 堆是用于存储创建好的对象和数组(数组也是对象)
2. JVM只有一个堆,被所有的线程共享
3. 堆是一个不连续的内存空间,分配灵活,速度慢
方法区(静态区):
1. JVM只有一个方法区,被所有线程共享,
2.方法区实际也是堆,只是用于存储类,常量相关信息
3.用来存放程序中永远是不变或者唯一的内容(类信息,class对象,静态变量,字符串常量等)
思考:本人思考如何实现一个100的阶乘 100!因为java的基本类型是长度是确定的,必定会出现溢出情况。
基本数据类型是放在栈中还是放在堆中,这取决于基本类型声明的位置。
一:在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因
在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。
(1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在方法栈中
(2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的。
二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁)。
同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量
(1)当声明的是基本类型的变量其变量名及其值放在堆内存中的
(2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中
根据这个,我将基本栈中的数据存放在堆中,用Array 来存储就可以解决这个问题
public class MulTest {
public static void main(String[] args) {
// System.out.println(capacity(9000));
// int num[] = numToarray(980);
// for(int i =0 ; i<num.length;i++){
// System.out.println("打印的第"+i+"位"+num[i]);
// }
int fnum[] = fn(4096);
printNum(fnum);
}
/**
* 确定一个数多少位,为方后边能给一个数组大小的空间
* @param n
* @return
*/
static int capacity(int num) {
int count = 0;
while (num > 0) {
num = num / 10; //比如说240 240/10 =24 count=1 ;24/10=2 count=2; 2/10=0 count=3
count++;
}
return count;
}
/**
* 将整型数字转换为数组
* numtoarray[0]= 0 ;第二次 num=240/10=24 ;numtoarray[1]=24%10=4;numtoarray[2]=2%10=2
* @param num
* @return
*/
static int [] numToarray(int num){
int len = capacity(num);
int numtoarray[] = new int [len];
for(int i=0;i<len;i++){
numtoarray[i] = num %10;
num = num/10;
}
return numtoarray;
}
/**
*
* 数组相乘,每一位数组与另一位数组相乘
*
*/
public static int[] mul(int []a, int []b){
int temp = 0;
int alength =a.length;
int blength = b.length;
int retult[] = new int[alength+blength] ;
for (int i = 0; i <= alength+blength - 1; i++) {
retult[i] = 0;
}
for (int i = 0; i <= alength - 1; i++) {
for (int j = 0; j <= blength - 1; j++) {
temp = a[i] * b[j];
retult[j + i] += temp;
}
}
int len = retult.length;
int temp2;
for (int i = 0; i <= retult.length-2; i++) {
temp2 = retult[i];
if (temp2 >= 10) {
retult[i] = temp2 % 10;
retult[i + 1] = retult[i + 1] + temp2 / 10;
}
}
return retult;
}
/**
* 阶乘
*/
public static int[] fn(int n){
int retult[] = {1};
int bit = capacity(n);
// System.out.println("返回多少位:"+n);
System.out.println("开始:"+n+"! = ");
int temp[] = new int[bit];
for(int i = 1;i<=n;i++){
temp =numToarray(i);
retult = mul(temp,retult);
}
return retult;
}
static void printNum(int a[]) {
int len = a.length;
int f = 0;
int count = 0;
for (int i = len - 1; i >= 0; i--) {
if (a[i] > 0)
f = 1;
if (f > 0) {
System.out.print(a[i]);
++count;
}
}
System.out.println();
System.out.println("----------------------------");
System.out.println("一共多少位:"+count);
}
}
构造器:
1. 通过New关键字调用,
2.构造器虽然没有返回值,但是不能定义返回值类型(返回值类型肯定是本类),不能在构造器里使用return返回某个值,但可以使用return结束
3.如果我们没有定义构造器,编译器会帮我们自动定义一个无参的构造器
如果已定义则编译器不会自动创建
4. 构造器的方法名必须和类名一致。
内存泄漏:
· 创建大量无用对象
比如,我们在需要大量拼接字符串时,使用了String而不是StringBuilder。
String str = "";
for (int i = 0; i < 10000; i++) {
str += i; //相当于产生了10000个String对象
}
静态集合类的使用
像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放。
· 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭
IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网络连接,不使用的时候一定要关闭。
· 监听器的使用
释放对象时,没有删除相应的监听器。
要点:
1. 程序员无权调用垃圾回收器。
2. 程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
3. finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用。
垃圾回收过程:
1、新创建的对象,绝大多数都会存储在Eden中,
2、当Eden满了(达到一定比例)不能创建新对象,则触发垃圾回收(GC),将无用对象清理掉,
然后剩余对象复制到某个Survivor中,如S1,同时清空Eden区
3、当Eden区再次满了,会将S1中的不能清空的对象存到另外一个Survivor中,如S2,
同时将Eden区中的不能清空的对象,也复制到S1中,保证Eden和S1,均被清空。
4、重复多次(默认15次)Survivor中没有被清理的对象,则会复制到老年代Old(Tenured)区中,
5、当Old区满了,则会触发一个一次完整地垃圾回收(FullGC),之前新生代的垃圾回收称为(minorGC)
在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
1.年老代(Tenured)被写满
2.持久代(Perm)被写满
3.System.gc()被显式调用(程序建议GC启动,不是调用GC)
4.上一次GC之后Heap的各域分配策略动态变化
多态:
1.多态是方法的多态,不是属性的多态(多态与属性无关)
2.多态的存在要有3个必要的条件:继承,方法重写,父类引用指向子类对象。
3.父类引用指向子类重写的方法,此时多态就出现了。
package com.test;
class Animal {
public void shout() {
System.out.println("叫了一声!");
}
}
class Dog extends Animal {
public void shout() {
System.out.println("旺旺旺!");
}
public void seeDoor() {
System.out.println("看门中....");
}
}
class Cat extends Animal {
public void shout() {
System.out.println("喵喵喵喵!");
}
}
public class TestPolym {
public static void main(String[] args) {
Animal a1 = new Cat(); // 向上可以自动转型
//传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
animalCry(a1);
Animal a2 = new Dog();
animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。
//编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
// 否则通不过编译器的检查。
Dog dog = (Dog)a2;//向下需要强制类型转换
dog.seeDoor();
}
// 有了多态,只需要让增加的这个类继承Animal类就可以了。
static void animalCry(Animal a) {
a.shout();
}
/* 如果没有多态,我们这里需要写很多重载的方法。
* 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。
static void animalCry(Dog d) {
d.shout();
}
static void animalCry(Cat c) {
c.shout();
}*/
}
package com.test;
public class HttpServer {
public void service(){
System.out.println("httpserver service");
doGet();
}
public void doGet(){
System.out.println("httpserver doGet");
}
}
package com.test;
public class MyServlet extends HttpServer{//继承
//方法重写
@Override
public void doGet() {
System.out.println("MyServlet doGet");
}
public static void main(String[] args) {
//父类引用指向子类重写的方法,此时多态就出现了
HttpServer MyServlet = new MyServlet();//父类引用指向子类对象
MyServlet.service();
}
}
final关键字的作用:
1. 修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
final int MAX_SPEED = 120;
2. 修饰方法:该方法不可被子类重写。但是可以被重载!
final void study(){}
3. 修饰类: 修饰的类不能被继承。比如:Math、String等。
抽象类:
抽象类是一种模板,抽象类为所有的子类提供一个通用的模板,子类可以在这个模板的基础上进行扩展
通过抽象类,可以避免子类设计的随意性,通过抽象类,我们就可以做到严格限制子类饿设计,是子类之间更加通用。
要点:
1. 有抽象方法的类只能定义成抽象类
2. 抽象类不能实例化,及不能用new来实例化抽象类
3. 抽象方法可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能被子类调用。
4. 抽象方法只能用来继承
5. 抽象方法必须被子类实现
接口:
[访问修饰符] interface 接口名 [extends 父接口1,父接口2…] {
常量定义;
方法定义;
}
interface MyInterface4extends MyInterface,MyInterface2,MyInterface3{
}
定义接口的详细说明:
1. 访问修饰符:只能是public或默认。
2. 接口名:和类名采用相同命名机制。
3. extends:接口可以多继承。
4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
5. 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。
要点
1. 子类通过implements来实现接口中的规范。
2. 接口不能创建实例,但是可用于声明引用变量类型。
3. 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
4. JDK1.7之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。
5. JDK1.8后,接口中包含普通的静态方法。
内部内:
1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。 但外部类不能访问内部类的内部属性。
3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方案变得更加完整。
内部类的使用场合:
1. 由于内部类提供了更好的封装特性,并且可以很方便的访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。
2. 使用内部类间接实现多继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。
在Java中内部类主要分为成员内部类(非静态内部类、静态内部类)、匿名内部类、局部内部类。
. 成员内部类(可以使用private、default、protected、public任意进行修饰。 类文件:外部类$内部类.class)
a) 非静态内部类(外部类里使用非静态内部类和平时使用其他类没什么不同)
i. 非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。
ii. 非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。
iii. 非静态内部类不能有静态方法、静态属性和静态初始化块。
iv. 外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。
v. 成员变量访问要点:
1. 内部类里方法的局部变量:变量名。
2. 内部类属性:this.变量名。
3. 外部类属性:外部类名.this.变量名。
注意
内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。
String:
String 类是不可改变的,所以你一旦创建了 String 对象,那它的值就无法改变了(详看笔记部分解析)。
如果需要对字符串做很多修改,那么应该选择使用 StringBuffer & StringBuilder 类。
StringBuffer & StringBuilder 类
StringBuilder:线程不安全,效率高,一般用它。
StrungBuffer:线程安全,但是效率低,一般不用。
String一经初始化后,就不会再改变其内容了。对String字符串的操作实际上是对其副本(原始拷贝)的操作,原来的字符串一点都没有改变。比如:
String s ="a"; 创建了一个字符串
s = s+"b"; 实际上原来的"a"字符串对象已经丢弃了,现在又产生了另一个字符串s+"b"(也就是"ab")。 如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的时间和空间性能,甚至会造成服务器的崩溃。
相反,StringBuilder和StringBuffer类是对原字符串本身操作的,可以对字符串进行修改而不产生副本拷贝或者产生少量的副本。因此可以在循环中使用。
String源码中数组是final固定的
StringBuilder源码中继承了抽象类AbstractStringBuilder,数组是可变的
无值的时候初始长度是16
有值的时候,默认是参数的长度+16
超过默认长度后,会增加
使用append()方法在字符串后面追加东西的时候,如果长度超过了该字符串存储空间大小了就需要进行扩容:构建新的存储空间更大的字符串,将旧的的复制过去;
再进行字符串append添加的时候,会先计算添加后字符串大小,传入一个方法:ensureCapacityInternal 这个方法进行是否扩容的判断,需要扩容就调用expandCapacity方法进行扩容:
char[] value;
int count;
尝试将新容量扩为 大小:变成2倍+2,容量如果还不够,直接扩充到需要的容量大小;
1.String
(1)类定义
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
String类是个final类,实现了序列化接口,比较大小接口和只读字符序列接口。String和其他八个基本数据类型的包装类共同为不可变类。
(2)主要变量
private final char value[];
String类的底层基本是char类型数组,String的一些基本方法也是调用char数组的基本方法。
(3)主要构造方法
public String() {
this.value = "".value;
}
String的默认值为"";
(4)String常量池
jvm在启动时就会加载的一块空间,符串常量池的特点就是有且只有一份相同的字面量,如果有其它相同的字面量,jvm则返回这个字面量的引用,如果没有相同的字面量,则在字符串常量池创建这个字面量并返回它的引用。通过使用new关键字得对象会放在堆里,而不会加载到字符串常量池,intern()方法能使一个位于堆中的字符串在运行期间动态地加入到字符串常量池中。
(5)字符串拼接
String字符串拼接一般通过用+号实现,正常情况下有两种形式:
String a = "ab" + "cd";
此时在JVM在编译时就认为这个加号是没有用处的,编译的时候就直接变成abcd,即使是后面添加多个字符串效率不会比StringBuiler或者StringBuffer慢。
String a = "ab";
String b = "cd";
String c = a + b;
此时字符串拼接系统会优化成通过new StringBuiler,进行两次append操作,该操作是在堆中进行,如果是进行多次拼接,会产生多个StringBuiler对象,效率自然会降低,但是如果是在同一行代码里做拼接操作,只是额外new了一个StringBuiler对象,效率也不会慢。
2.StringBuilder
(3)主要构造方法
public StringBuilder() {
super(16);
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
StringBuiler类的基本构造方法是一个容量为16的字符串数组
(4)字符串拼接
public StringBuilder append(String str) {
super.append(str);
return this;
}
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
内部的实现是通过char数组操作的,如果超过容量,会发生通过数组拷贝方式的扩容操作。
(5)线程安全性
从上面的源码解析,count += len;并非原子操作,如果两个线程同时访问到这个方法,那么AbstractStringBuilder中的count是不是就是相同的,所以这两个线程都是在底层char数组的count位置开始append添加,那么最终的结果肯定就是在后执行的那个线程append进去的数据会将前面一个覆盖掉,所以StringBuilder在字符串拼接操作是线程不安全的。由于String的拼接也是通过StringBuiler来实现,所以String的字符拼接也不是线程安全的
3.StringBuffer
(1)类定义
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
StringBuffer其父类实现了只读字符序列接口和拼接接口,与StringBuilder有公共的父类AbstractStringBuilder,该类是可变类。
(2)主要变量
StringBuffer的底层基本实现和StringBuilder是一致的。
(3)构造方法
StringBuffer的构造方法和StringBuilder是一致的。
(4)字符串拼接
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuffer字符串拼接基本与StringBuilder内部实现是一致的,主要是区别是在方法体上通过synchronized关键字加锁。
(5)线程安全性
从上面的源码解析,由于在方法加上synchronize关键字,所以字符串拼接操作是线程安全。
备注:在正式面试时,基本回答三者的区别是:String是不可变类,StringBuffer和StringBuilder是可变类;String在多次字符串拼接时效率低,且线程不安全,StringBuilsder效率最高,但是线程不安全,StringBuffer效率在前两者其中,但是线程安全。