java后端知识

19 篇文章 1 订阅
1 篇文章 0 订阅

JAVA 基础知识点

什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。
Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变成可能,因为它知道底层硬件平台的指令长度和其它特性。

什么是J2EE?JVM?JRE?JDK?

(1)J2EE:是为开发企业环境下的应用程序提供的一套解决方案,该技术体系中包含的技术如Servlet、Jsp等,主要针对Web应用程序开发。
(2)JVM:JVM是java虚拟机(JVM Java Virtual Machine),java程序需要运行在虚拟机上,不同平台有自己的虚拟机,因此java语言可以跨平台。
(3)JRE:包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需的核心类库等如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。JRE:JVM+类库。
(4)JDK:JDK是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了JRE。所以安装了JDK,就不用在单独安装JRE了。其中的开发工具:编译工具(javac.exe) 打包工具(jar.exe)等。JDK:JRE+JAVA的开发工具。

static关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static方法?

“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。

Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。

Java中也不可以覆盖private方法,因为private修饰的变量和方法只能在当前类中使用,如果是其它类继承当前类,是不能访问到private变量或方法的,当然也不能覆盖。

是否可以在static环境中访问非static变量?

static变量在Java中是属于类的,它在所有的实例中的值是一样的,当类被Java虚拟机载入的时候,会对static变量进行初始化。如果你的代码尝试不用实例来访问非static的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。

Java支持的数据类型有哪些?什么是自动拆装箱?

Java语言支持的8种基本数据类型是:

boolean

byte、char

short、int

long、float、double

自动装箱是Java编译器在基本数据类型和对应的对象包装类型之间做的一个转化。比如:把int转化成integer,double转化成Double,等等。反之就是自动拆箱。

面向对象的三大特性(继承、封装、多态)

继承、封装、多态

什么是继承?
①继承是面向对象程序设计能够提高软件开发效率的重要原因之一。
②继承是具有传递性的,就像现实中孙子不仅长得像爸爸而且还像他爷爷。
③继承来的属性和方法是隐式的,也就是在本类里面是看不见的。
④一个类只能有一个父类,也就是类只能是单继承。
⑤一个接口可以有多个父类,也就是接口可以是多继承。
实际项目开发中,一个类继承于另一个类,那么前者就是后者的子类,反则反之。

什么是封装?
对象数据和操作该对象的指令都是对象自身的一部分,能够实现尽可能对外部隐藏数据。
实际项目开发中,使用封装最多的就是实体类,常常和JavaBean(类必须是具体的和公共的,并且具有无参数的构造器)一起使用。
那么,实体类有那些东西呢?
答:私有的成员变量、无参数的构造器、有参数的构造器、setter和getters方法、重写tostring方法、重写hashCode和equals方法。

什么是多态?
①多态就是对象拥有多种形态:引用多态和方法多态。
②引用多态:父类的引用可以指向本类对象、父类的引用可以指向子类的对象。
③方法多态:创建本类对象时,调用的方法为本类的方法;创建子类对象时,调用的方法为子类重写的方法或者继承的方法。
④存在多态的必要条件:继承、重写。
⑤多态的作用是消除类型之间的耦合关系。
在实际项目开发中,A类继承B类,如果在A类中不重写B类的方法的时候,输出的仍旧是B类方法里面的信息(B b=new A());如果在A类中重写B类的方法的时候,输出的是A类方法里面的信息(B b=new A())。

java为什么不支持多继承?
1.若子类继承的父类中拥有相同的成员变量,子类在引用该变量时将无法判别使用哪个父类的成员变量。
2.若一个子类继承的多个父类拥有相同方法,同时子类并未覆盖该方法(若覆盖,则直接使用子类中该方法),那么调用该方法时将无法确定调用哪个父类的方法。

Java 中覆盖和重载是什么意思?
解析:覆盖和重载是比较重要的基础知识点,并且容易混淆,所以面试中常见。
答:覆盖(Overide)是指子类对父类方法的一种重写,只能比父类抛出更少的异常,访问权限不能比父类的小。
被覆盖的方法不能是 private 的,否则只是在子类中重新定义了一个方法;

重载(Overload)表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同。

面试官: 那么构成重载的条件有哪些?

答:参数类型不同、参数个数不同、参数顺序不同。

面试官: 函数的返回值不同可以构成重载吗?为什么?

答:不可以,因为 Java 中调用函数并不需要强制赋值。举例如下:
如下两个方法:

void f(){}
int f(){ return 1;}

只要编译器可以根据语境明确判断出语义,比如在 int x = f();中,那么的确可以据此区分重载方法。不过, 有时你并不关心方法的返回值,你想要的是方法调用的其他效果 (这常被称为 “为了副作用而调用”),这时你可能会调用方法而忽略其返回值,所以如果像下面的调用:

fun();

此时 Java 如何才能判断调用的是哪一个 f() 呢?别人如何理解这种代码呢?所以,根据方法返回值来区分重载方法是行不通的。

重定向和转发的区别。

1、重定向是两次请求,转发是一次请求,因此转发的速度要快于重定向
2、重定向之后地址栏上的地址会发生变化,变化成第二次请求的地址,转发之后地址栏上的地址不会变化,还是第一次请求的地址
3、转发是服务器行为,重定向是客户端行为。重定向时浏览器上的网址改变 ,转发是浏览器上的网址不变
4、重定向是两次request,转发只有一次请求
5、重定向时的网址可以是任何网址,转发的网址必须是本站点的网址

抽象类和接口的区别有哪些?

答:
抽象类中可以没有抽象方法;接口中的方法必须是抽象方法;
抽象类中可以有普通的成员变量;接口中的变量必须是 static final 类型的,必须被初始化 , 接口中只有常量,没有变量。
抽象类只能单继承,接口可以继承多个父接口;
Java8 中接口中会有 default 方法,即方法可以被实现。

面试官:抽象类和接口如何选择?

答:
如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。
如果知道某个类应该是基类,那么第一个选择的应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。因为抽象类中允许存在一个或多个被具体实现的方法,只要方法没有被全部实现该类就仍是抽象类。

Java 和 C++ 的区别:

答:都是面向对象的语言,都支持封装、继承和多态;
指针:Java 不提供指针来直接访问内存,程序更加安全;
继承: Java 的类是单继承的,C++ 支持多重继承; Java 通过一个类实现多个接口来实现 C++ 中的多重继承; Java 中类不可以多继承,但是!!!接口可以多继承;
内存: Java 有自动内存管理机制,不需要程序员手动释放无用内存。

&与&&的区别:

&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。

equals和==

==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;
equals():比较的是两个字符串的内容,属于内容比较。

ArrayList简介

**ArrayList 的底层是数组队列,相当于动态数组。**与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。

它继承于 AbstractList,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。

在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为O(n),求表长以及增加元素,取第 i 元素的时间复杂度为O(1)

ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。

和 Vector 不同,ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。
ArrayList的遍历我们有三种方式:for循环,增强for循环 和 迭代器三种方式。

链表的代码实现

#include <string>
#include <iostream>
using namespace std;

typedef int DataType;

class Node
{
public:
	DataType data;
	Node *next;
};

class LinkList
{
public:
	LinkList();
	~LinkList();
	int CreateLinkList(int size);
	int BYELinkList();
	int TravalLinkList();
	int InsertLinklList(Node *data, int n);
	int DeleteLinklist(int n);

	int GetLen();
	bool IsEmply();

	Node *head;
	int size;
};

LinkList::LinkList()
{
	head = new Node;
	head->data = 0;
	head->next = NULL;
	size = 0;
}

LinkList::~LinkList()
{
	delete head;
}

int LinkList::CreateLinkList(int n)
{
	if (n<0) {
		printf("error\n");
		return -1;
	}
	Node *ptemp = NULL;
	Node *pnew = NULL;
	
	this->size = n;
	ptemp = this->head;
	for(int i =0 ; i<n ; i++)
	{
		pnew = new Node;
		pnew->next = NULL;
		cout << "输入第" << i+1 << "个节点值" << endl;
		cin >> pnew->data;
		ptemp->next = pnew;
		ptemp = pnew;
	}
	cout << "创建完成" << endl;
	return 0;
}

int LinkList::BYELinkList()
{
	Node *ptemp;
	if (this->head == NULL) {
		cout << "链表原本就为空" << endl;
		return -1;
	}
	while (this->head)
	{
		ptemp = head->next;
		free(head);
		head = ptemp;
	}
	cout << "销毁链表完成" << endl;
	return 0;
}

int LinkList::TravalLinkList()
{
	Node *ptemp = this->head->next;
	if (this->head == NULL) {
		cout << "链表为空" << endl;
		return -1;
	}
	while(ptemp)
	{
		cout << ptemp->data << "->";
		ptemp = ptemp->next;
	}
	cout <<"NULL"<< endl;
	return 0;
}

int LinkList::InsertLinklList(Node *data, int n)
{
	Node *ptemp;
	if (this->head == NULL) {
		cout << "链表为空" << endl;
		return -1;
	}
	if (data == NULL) {
		cout << "插入节点为空" << endl;
		return -1;
	}
	//头插
	if (n<2) {
		Node *pnew = new Node;
		pnew->data = data->data;
		pnew->next = this->head->next;
		this->head->next = pnew;
		this->size++;
		return 0;
	}
	//尾插
	if (n > this->size) {
		ptemp = this->head;
		while (ptemp->next != NULL) {
			ptemp = ptemp->next;
		}
		Node *pnew = new Node;
		pnew->data = data->data;
		pnew->next = NULL;
		ptemp->next = pnew;
		this->size++;
		return 0;
	}
	//中间插
	else {
		ptemp = this->head;
		for (int i = 1; i < n; i++) {
			ptemp = ptemp->next;
		}
		Node *pnew = new Node;
		pnew->data= data->data;
		pnew->next = ptemp->next;
		ptemp->next = pnew;
		this->size++;
		return 0;
	}
}

int LinkList::DeleteLinklist(int n)
{
	Node *ptemp;
	Node *ptemp2;
	if (n > this->size) {
		cout << "n太大" << endl;
		return -1;
	}
	//删头节点
	if (n < 2) {
		ptemp = this->head->next;
		this->head->next = ptemp->next;
		free(ptemp);
		this->size--;
		return 0;
	}
	//尾部删除
	if (n == this->size) {
		ptemp = this->head;
		for (int i = 1; i < this->size;i++) {
			ptemp = ptemp->next;
		}
		ptemp2 = ptemp->next;
		ptemp->next = NULL;
		free(ptemp2);
		this->size--;
		return 0;
	}
	//中间删除
	else
	{
		ptemp = this->head;
		for (int i = 1; i < n; i++) {
			ptemp = ptemp->next;
		}
		ptemp2 = ptemp->next;
		ptemp->next = ptemp2->next;
		free(ptemp2);
		this->size--;
		return 0;
	}
}

int LinkList::GetLen()
{
	return this->size;
}

bool LinkList::IsEmply()
{
	if (this->head == NULL) {
		return true;
	}
	else{
		return false;
	}
}

void main(void)
{
	LinkList list;
	LinkList *plist = &list;
	plist->CreateLinkList(5);
	plist->TravalLinkList();
	Node temp;
	temp.data = 100;
	temp.next = NULL;
	plist->InsertLinklList(&temp, 0);
	plist->TravalLinkList();
	plist->InsertLinklList(&temp, plist->GetLen()+1);
	plist->TravalLinkList();
	plist->InsertLinklList(&temp, 5);

	plist->TravalLinkList();
	plist->DeleteLinklist(0);
	plist->TravalLinkList();
	plist->DeleteLinklist(list.GetLen());
	plist->TravalLinkList();
	plist->DeleteLinklist(2);
	plist->TravalLinkList();


	plist->BYELinkList();
	system("pause");
}

Java 中常见集合(重点)

集合这方面的考察相当多,这部分是面试中必考的知识点。

1)说说常见的集合有哪些吧?
答:Map 接口和 Collection 接口是所有集合框架的父接口:

Collection 接口的子接口包括:Set 接口和 List 接口;
Map 接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap 以及 Properties 等;
Set 接口的实现类主要有:HashSet、TreeSet、LinkedHashSet 等;
List 接口的实现类主要有:ArrayList、LinkedList、Stack 以及 Vector 等。

2)HashMap 和 Hashtable 的区别有哪些?(必问)
答:
HashMap 没有考虑同步,是线程不安全的;Hashtable 使用了 synchronized 关键字,是线程安全的;
前者允许 null 作为 Key;后者不允许 null 作为 Key。

3)HashMap 的底层实现你知道吗?
答:在 Java8 之前,其底层实现是数组 + 链表实现,Java8 使用了数组 + 链表 + 红黑树实现。
主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。

4)ConcurrentHashMap 和 Hashtable 的区别?(必问)
答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,hashtable 考虑了同步的问题。但是 hashtable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。 ConcurrentHashMap 将 hash 表分为 16 个桶(默认值),诸如 get,put,remove 等常用操作只锁当前需要用到的桶。

面试官:ConcurrentHashMap 的具体实现知道吗?
答:
该类包含两个静态内部类 HashEntry 和 Segment;前者用来封装映射表的键值对,后者用来充当锁的角色;
Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个 HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

5)HashMap 的长度为什么是 2 的幂次方?
答:
通过将 Key 的 hash 值与 length-1 进行 & 运算,实现了当前 Key 的定位,2 的幂次方可以减少冲突(碰撞)的次数,提高 HashMap 查询效率;
如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111……的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;
如果 length 不是 2 的次幂,比如 length 为 15,则 length-1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大。
更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。

6)List 和 Set 的区别是啥?
答:List 元素是有序的,可以重复;Set 元素是无序的,不可以重复。

7)List、Set 和 Map 的初始容量和加载因子
答:

List
ArrayList 的初始容量是 10;加载因子为 0.5; 扩容增量:原容量的 0.5 倍 +1;一次扩容后长度为 16。
Vector 初始容量为 10,加载因子是 1。扩容增量:原容量的 1 倍,如 Vector 的容量为 10,一次扩容后是容量为 20。
Set
HashSet,初始容量为 16,加载因子为 0.75; 扩容增量:原容量的 1 倍; 如 HashSet 的容量为 16,一次扩容后容量为 32
Map
HashMap,初始容量 16,加载因子为 0.75; 扩容增量:原容量的 1 倍; 如 HashMap 的容量为 16,一次扩容后容量为 32
8)Comparable 接口和 Comparator 接口有什么区别?
答:
前者简单,但是如果需要重新定义比较类型时,需要修改源代码。
后者不需要修改源代码,自定义一个比较器,实现自定义的比较方法。

9)Java 集合的快速失败机制 “fail-fast”
答:它是 java 集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如 :假设存在两个线程(线程 1、线程 2),线程 1 通过 Iterator 在遍历集合 A 中的元素,在某个时候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。

原因: 迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。

每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

解决办法:

在遍历过程中,所有涉及到改变 modCount 值得地方全部加上 synchronized;
使用 CopyOnWriteArrayList 来替换 ArrayList。

高并发编程-JUC 包

在 Java 5.0 提供了 java.util.concurrent(简称 JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。

多线程和单线程的区别和联系:

答:
在单核 CPU 中,将 CPU 分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流占用 CPU 的机制。
多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需要的时间比一个线程的进程执行两次所需要的时间要多一些。
结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间。

如何指定多个线程的执行顺序?

解析:面试官会给你举个例子,如何让 10 个线程按照顺序打印 0123456789?(写代码实现)
答:
设定一个 orderNum,每个线程执行结束之后,更新 orderNum,指明下一个要执行的线程。并且唤醒所有的等待线程。
在每一个线程的开始,要 while 判断 orderNum 是否等于自己的要求值!!不是,则 wait,是则执行本线程。

线程和进程的区别:(必考)

答:
进程是一个 “执行中的程序”,是系统进行资源分配和调度的一个独立单位;
线程是进程的一个实体,一个进程中拥有多个线程,线程之间共享地址空间和其它资源(所以通信和同步等操作线程比进程更加容易);
线程上下文的切换比进程上下文切换要快很多。
(1)进程切换时,涉及到当前进程的 CPU 环境的保存和新被调度运行进程的 CPU 环境的设置。
(2)线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。

多线程产生死锁的 4 个必要条件?

答:
**互斥条件:**一个资源每次只能被一个线程使用;
**请求与保持条件:**一个线程因请求资源而阻塞时,对已获得的资源保持不放;
**不剥夺条件:**进程已经获得的资源,在未使用完之前,不能强行剥夺;
**循环等待条件:**若干线程之间形成一种头尾相接的循环等待资源关系。
面试官:如何避免死锁?(经常接着问这个问题哦~)
答:指定获取锁的顺序,举例如下:
比如某个线程只有获得 A 锁和 B 锁才能对某资源进行操作,在多线程条件下,如何避免死锁?
获得锁的顺序是一定的,比如规定,只有获得 A 锁的线程才有资格获取 B 锁,按顺序获取锁就可以避免死锁!!!

5)sleep( ) 和 wait( n)、wait( ) 的区别:
答:
sleep 方法:是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话);
wait 方法:是 Object 的方法,必须与 synchronized 关键字一起使用,线程进入阻塞状态,当 notify 或者 notifyall 被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,释放互斥锁。

6)synchronized 关键字:
答:底层实现:
进入时,执行 monitorenter,将计数器 +1,释放锁 monitorexit 时,计数器-1;
当一个线程判断到计数器为 0 时,则当前锁空闲,可以占用;反之,当前线程进入等待状态。
含义:(monitor 机制)
Synchronized 是在加锁,加对象锁。对象锁是一种重量锁(monitor),synchronized 的锁机制会根据线程竞争情况在运行时会有偏向锁(单一线程)、轻量锁(多个线程访问 synchronized 区域)、对象锁(重量锁,多个线程存在竞争的情况)、自旋锁等。
该关键字是一个几种锁的封装。

7)volatile 关键字
答:该关键字可以保证可见性不保证原子性。
功能:
主内存和工作内存,直接与主内存产生交互,进行读写操作,保证可见性;
禁止 JVM 进行的指令重排序。
解析:关于指令重排序的问题,可以查阅 DCL 双检锁失效相关资料。

8)ThreadLocal(线程局部变量)关键字:
答:当使用 ThreadLocal 维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。

ThreadLocal 内部实现机制:

每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;
Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;
Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。

9)Atomic 关键字:
答:可以使基本数据类型以原子的方式实现自增自减等操作。参考我的博客:concurrent.atomic 包下的类 AtomicInteger 的使用。

10)线程池有了解吗?(必考)
答:java.util.concurrent.ThreadPoolExecutor 类就是一个线程池。客户端调用 ThreadPoolExecutor.submit(Runnable task) 提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有 3 种形态:

当前线程池大小 :表示线程池中实际工作者线程的数量;
最大线程池大小 (maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限;
核心线程大小 (corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限。
如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队;
如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不是添加新线程;
如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出 maxinumPoolSize, 在这种情况下,任务将被拒绝。

实现多线程的四种方式

备注:基础上都是围绕Runnable接口,与Thread类进行扩展的

第一种:继承Thread类,重写它的Run方法。

public class Test_thread extends Thread{
 
    @Override
    public void run() {
        
        //重写编写父类
        System.out.println("我是Thread 方式");
        //输出一个循环
        for (int i = 0; i < 2; i++) {
                System.out.print(i+ "  ");
        }
    }
    public static void main(String[] args) {
        Test_thread  t = new Test_thread();
        Thread t2 = new Thread(t);
        t2.start();  
    }
}

第二种:实现Runnable 接口,重写run方法

public class Test_Runnable  implements Runnable{

	@Override
	public void run() {
		//实现父类的方法
		System.out.println("我是Runnable 方式实现多线程");
		
	}
	
	public static void main(String[] args) {
		Test_Runnable t =new Test_Runnable();
		new Thread(t).start();
	}
 
}

第三种:实现callable接口,重写这个call()方法

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
 
public class Test_callable implements Callable<Object>{
 
	@Override
	public Object call() throws Exception {
		
		return "我是callable";
	}
	
	public static void main(String[] args) throws Exception{
		Test_callable t =new Test_callable();
		
		FutureTask<Object> task = new FutureTask<>(t);
		new Thread(task).start();
		
		
		System.out.println(task.get());
		
	}
 
}

第四种:采用这个线程池的方式,ExecutorService对象

public class Test_threadPool {
 
	
	
	/**
	 * 利用线程池:
	 * 先要实现这个Runnable 接口
	 * 后在实例化这个ExecutorService。
	 * @param args
	 */
	public static void main(String[] args) {
		
                // 下面的这条语句是在线程池中定义5个线程容量
		ExecutorService executorService =Executors.newFixedThreadPool(5);
		// 下面的意思为,executorservice执行一个new A()对象,
                executorService.execute(new A());
                //下面的表示终结所有的线程对象
		executorService.shutdown();
	}
	
}
 
class A implements Runnable{
 
	@Override
	public void run() {
		System.out.println(" A ");
	}
	
}

线程池

在这里插入图片描述

从源码中可以看出,线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。下面会对这7个参数一一解释。

一、corePoolSize 线程池核心线程大小

线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。

二、maximumPoolSize 线程池最大线程数量

一个任务被提交到线程池后,首先会缓存到工作队列(后面会介绍)中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。 一个新任务提交给线程池,应该是先判断线程池线程数是否≥核心线程数,如果线程池线程数≥核心线程数且任务队列未满时,会将该任务提交到任务队列,任务队列已满则执行拒绝策略;如果线程池线程数<核心线程数,那会创建一个新线程(核心线程)执行该任务。

三、keepAliveTime 空闲线程存活时间

一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定

四、unit 空闲线程存活时间单位

keepAliveTime的计量单位

五、workQueue 工作队列

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

①ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

六、threadFactory 线程工厂

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等

七、handler 拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:

①CallerRunsPolicy

该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。

②AbortPolicy

该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。

③DiscardPolicy

该策略下,直接丢弃任务,什么都不做。

④DiscardOldestPolicy

该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

线程的状态

新建状态:新建线程对象,并没有调用start()方法之前

就绪状态:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦。

运行状态:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态

阻塞状态:线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态

死亡状态:线程执行结束

锁类型

可重入锁:在执行对象中所有同步方法不用再次获得锁

可中断锁:在等待获取锁过程中可中断

公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利

读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

synchronized与Lock的区别

在这里插入图片描述

JVM

堆栈

1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制.
2. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中。)
3. 堆:存放所有new出来的对象。
4. 静态域:存放静态成员(static定义的)
5. 常量池:存放字符串常量和基本类型常量(public static final)。
6. 非RAM存储:硬盘等永久存储空间

Java的垃圾回收

为什么要进行垃圾回收?
随着程序的运行,内存中存在的实例对象、变量等信息占据的内存越来越多,如果不及时进行垃圾回收,必然会带来程序性能的下降,甚至会因为可用内存不足造成一些不必要的系统异常。

哪些“垃圾”需要回收?
如果某个对象已经不存在任何引用,那么它可以被回收。

什么时候进行垃圾回收?
引用计数算法
每个对象添加一个引用计数器,每被引用一次,计数器加1,失去引用,计数器减1,当计数器在一段时间内保持为0时,该对象就认为是可以被回收得了。(在JDK1.2之前,使用的是该算法)

缺点:当两个对象A、B相互引用的时候,当其他所有的引用都消失之后,A和B还有一个相互引用,此时计数器各为1,而实际上这两个对象都已经没有额外的引用了,已经是垃圾了。但是却不会被回收

可达性分析算法
该算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点

数据库

IN和EXISTS的区别

IN先执行子查询,EXISTS先执行主查询。IN不对NULL进行处理。in 是把外表和内表作hash 连接,而exists是对外表作loop循环,每次loop循环再对内表进行查询。一直以来认为exists比in效率高的说法是不准确的。
not in 和not exists
如果查询语句使用了not in 那么内外表都进行全表扫描,没有用到索引;而not extsts 的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。

数据保存在数据库

1)数据永久保存

2)使用SQL语句,查询方便效率高。

3)管理数据方便

什么是SQL?

结构化查询语言(Structured Query Language)简称SQL,是一种数据库查询语言。

作用:用于存取数据、查询、更新和管理关系数据库系统。

什么是MySQL?

MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。

数据库三大范式是什么

第一范式:每个列都不可以再拆分。

第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。

第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。

索引

什么是索引?
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。

索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。

更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。

什么是数据库事务?

事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

特点
原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

什么是脏读?幻读?不可重复读?

脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

Spring框架

Spring类加载方式:

注解
xml配置文件

Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。

@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

@ComponentScan:Spring组件扫描。

什么是 Spring Boot?

Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,使开发者能快速上手。

为什么要用SpringBoot

快速开发,快速整合,配置简化、内嵌服务容器
SpringBoot与SpringCloud 区别
SpringBoot是快速开发的Spring框架,SpringCloud是完整的微服务框架,SpringCloud依赖于SpringBoot。

Spring Boot 有哪些优点?

Spring Boot 主要有如下优点:

容易上手,提升开发效率,为 Spring 开发提供一个更快、更简单的开发框架。
开箱即用,远离繁琐的配置。
提供了一系列大型项目通用的非业务性功能,例如:内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。
SpringBoot总结就是使编码变简单、配置变简单、部署变简单、监控变简单等等

Spring Boot 有哪几种读取配置的方式?

Spring Boot 可以通过
@PropertySource,
@Value
,@Environment,
@ConfigurationPropertie注解来绑定变量
你如何理解 Spring Boot 配置加载顺序?

在 Spring Boot 里面,可以使用以下几种方式来加载配置。

1.properties文件;

2.YAML文件;

3.系统环境变量;

4.命令行参数;

IOC

依赖注入
DI,英文全称,Dependency Injection,意为依赖注入。

依赖注入:由IoC容器动态地将某个对象所需要的外部资源(包括对象、资源、常量数据)注入到组件(Controller, Service等)之中。简单点说,就是IoC容器会把当前对象所需要的外部资源动态的注入给我们。

Spring依赖注入的方式主要有四个,基于注解注入方式、set注入方式、构造器注入方式、静态工厂注入方式。推荐使用基于注解注入方式,配置较少,比较方便。

基于注解注入方式

服务层代码

@Service
public class AdminService {
    //code
}
控制层代码

@Controller
@Scope("prototype")
public class AdminController {
 
    @Autowired
    private AdminService adminService;
 
    //code
}

@Autowired与@Resource都可以用来装配Bean,都可以写在字段、setter方法上。他们的区别是:

@Autowired默认按类型进行自动装配(该注解属于Spring),默认情况下要求依赖对象必须存在,如果要允许为null,需设置required属性为false,例:@Autowired(required=false)。如果要使用名称进行装配,可以与@Qualifier注解一起使用。

@Autowired
@Qualifier("adminService")
private AdminService adminService;

@Resource默认按照名称进行装配(该注解属于J2EE),名称可以通过name属性来指定。如果没有指定name属性,当注解写在字段上时,默认取字段名进行装配;如果注解写在setter方法上,默认取属性名进行装配。当找不到与名称相匹配的Bean时,会按照类型进行装配。但是,name属性一旦指定,就只会按照名称进行装配。

@Resource(name = "adminService")
private AdminService adminService;

面向切面编程(AOP)

面向切面编程(AOP)就是纵向的编程。比如业务A和业务B现在需要一个相同的操作,传统方法我们可能需要在A、B中都加入相关操作代码,而应用AOP就可以只写一遍代码,A、B共用这段代码。并且,当A、B需要增加新的操作时,可以在不改动原代码的情况下,灵活添加新的业务逻辑实现。

在实际开发中,比如商品查询、促销查询等业务,都需要记录日志、异常处理等操作,AOP把所有共用代码都剥离出来,单独放置到某个类中进行集中管理,在具体运行时,由容器进行动态织入这些公共代码。

AOP主要一般应用于签名验签、参数校验、日志记录、事务控制、权限控制、性能统计、异常处理等。

AOP涉及名词
切面(Aspect):共有功能的实现。如日志切面、权限切面、验签切面等。在实际开发中通常是一个存放共有功能实现的标准Java类。当Java类使用了@Aspect注解修饰时,就能被AOP容器识别为切面。

通知(Advice):切面的具体实现。就是要给目标对象织入的事情。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际开发中通常是切面类中的一个方法,具体属于哪类通知,通过方法上的注解区分。

连接点(JoinPoint):程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出等。Spring只支持方法级的连接点。一个类的所有方法前、后、抛出异常时等都是连接点。

切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。

比如,在上面所说的连接点的基础上,来定义切入点。我们有一个类,类里有10个方法,那就产生了几十个连接点。但是我们并不想在所有方法上都织入通知,我们只想让其中的几个方法,在调用之前检验下入参是否合法,那么就用切点来定义这几个方法,让切点来筛选连接点,选中我们想要的方法。切入点就是来定义哪些类里面的哪些方法会得到通知。

目标对象(Target):那些即将切入切面的对象,也就是那些被通知的对象。这些对象专注业务本身的逻辑,所有的共有功能等待AOP容器的切入。

代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象本身业务逻辑加上共有功能。代理对象对于使用者而言是透明的,是程序运行过程中的产物。目标对象被织入共有功能后产生的对象。

织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译时、类加载时、运行时。Spring是在运行时完成织入,运行时织入通过Java语言的反射机制与动态代理机制来动态实现。

Pointcut用法
Pointcut格式为:

execution(modifier-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
修饰符匹配 modifier-pattern? 例:public private

返回值匹配 ret-type-pattern 可以用 * 表示任意返回值

类路径匹配 declaring-type-pattern? 全路径的类名

方法名匹配 name-pattern 可以指定方法名或者用 * 表示所有方法;set* 表示所有以set开头的方法

参数匹配 (param-pattern) 可以指定具体的参数类型,多个参数用“,”分隔;可以用 * 表示匹配任意类型的参数;可以用 (…) 表示零个或多个任意参数

异常类型匹配throws-pattern? 例:throws Exception

其中后面跟着 ? 表示可选项

例:

@Pointcut("execution(public * cn.wbnull. springbootdemo.controller.*.*(..))")
private void sign() {
 
}
 

3.3 一个例子
以 Spring Boot入门:使用AOP实现拦截器 中的AOP为例

@Aspect
@Component
public class SignAop {
 
}
SignAop类使用了@Aspect注解,则该类可以被AOP容器识别为切面。

 

@Aspect
@Component
public class SignAop {
 
    @Pointcut("execution(public * cn.wbnull.springbootdemo.controller.*.*(..))")
    private void signAop() {
 
    }
}

@Pointcut声明一个切入点,范围为controller包下所有的类的所有方法

注:作为切入点签名的方法必须返回void类型

@Aspect
@Component
public class SignAop {
 
    @Pointcut("execution(public * cn.wbnull.springbootdemo.controller.*.*(..))")
    private void signAop() {
 
    }
 
    @Before("signAop()")
    public void doBefore(JoinPoint joinPoint) throws Exception {
        //code
       }
 
    @AfterReturning(value = "signAop()", returning = "params")
    public JSONObject doAfterReturning(JoinPoint joinPoint, JSONObject params) {
        //code
        }
}

doBefore()方法使用@Before(“signAop()”)注解,表示前置通知(在某连接点之前执行的通知),但这个通知不能阻止连接点之前的执行流程,除非它抛出一个异常。

doAfterReturning()方法使用@AfterReturning(value = “signAop()”, returning = “params”)注解,表示后置通知(在某连接点正常完成后执行的通知),通常在一个匹配的方法返回的时候执行。

实际运行时,在进入controller包下所有方法前,都会进入doBefore()方法,在controller包下方法执行完成后,都会进入doAfterReturning()方法。

网络协议相关

浏览器中输入:“www.xxx.com” 之后都发生了什么?请详细阐述。

  1. 由域名→IP 地址

  2. 寻找 IP 地址的过程依次经过了浏览器缓存、系统缓存、hosts 文件、路由器缓存、 递归搜索根域名服务器。

  3. 建立 TCP/IP 连接(三次握手具体过程)

  4. 由浏览器发送一个 HTTP 请求

  5. 经过路由器的转发,通过服务器的防火墙,该 HTTP 请求到达了服务器

  6. 服务器处理该 HTTP 请求,返回一个 HTML 文件

  7. 浏览器解析该 HTML 文件,并且显示在浏览器端
    这里需要注意:
    HTTP 协议是一种基于 TCP/IP 的应用层协议,进行 HTTP 数据请求必须先建立 TCP/IP 连接
    可以这样理解:HTTP 是轿车,提供了封装或者显示数据的具体形式;Socket 是发动机,提供了网络通信的能力。
    两个计算机之间的交流无非是两个端口之间的数据通信 , 具体的数据会以什么样的形式展现是以不同的应用层协议来定义的。

5)常见 HTTP 状态码

1xx(临时响应)
2xx(成功)
3xx(重定向):表示要完成请求需要进一步操作
4xx(错误):表示请求可能出错,妨碍了服务器的处理
5xx(服务器错误):表示服务器在尝试处理请求时发生内部错误
常见状态码:
200(成功)
304(未修改):自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容
401(未授权):请求要求身份验证
403(禁止):服务器拒绝请求
404(未找到):服务器找不到请求的网页

6)TCP 和 UDP 的区别:

回答发送数据前是否存在建立连接的过程;
TCP过确认机制,丢包可以重发,保证数据的正确性;UDP不保证正确性,只是单纯的负责发送数据包;
UDP 是面向报文的。发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付给 IP 层。既不拆分,也不合并,而是保留这些报文的边界,因 此,应用程序需要选择合适的报文大小;
UDP 的头部,只有 8 个字节,相对于 TCP 头部的 20 个字节信息包的额外开销很小。

Linux 常见命令

作者对这一方面不是很精通,知识点来源于网络总结以及面试官的提问,仅供小伙伴参考。
1)grep、sed 以及 awk 命令

解析:awk 命令如果可以掌握,是面试中的一个 加分点。

2)文件和目录:

pwd 显示当前目录
ls 显示当前目录下的文件和目录:

ls -F 可以区分文件和目录;
ls -a 可以把隐藏文件和普通文件一起显示出来;
ls -R 可以递归显示子目录中的文件和目录;
ls -l 显示长列表;
ls -l test 过滤器,查看某个特定文件信息。可以只查看 test 文件的信息。
3)处理文件方面的命令有:touch、cp、 In、mv、rm、

4)处理目录方面的命令:mkdir

5)查看文件内容:file、cat、more、less、tail、head

6)监测程序命令:ps、top

eg. 找出进程名中包括 java 的所有进程:ps -ef | grep java

top 命令 实时监测进程

top 命令输出的第一部分:显示系统的概括。

第一行显示了当前时间、系统的运行时间、登录的用户数和系统的平均负载(平均负载有 3 个值:最近 1min 5min 15min);
第二行显示了进程的概要信息,有多少进程处于运行、休眠、停止或者僵化状态;
第三行是 CPU 的概要信息;
第四行是系统内存的状态。
7)ps 和 top 命令的区别:

ps 看到的是命令执行瞬间的进程信息 , 而 top 可以持续的监视;
ps 只是查看进程 , 而 top 还可以监视系统性能 , 如平均负载 ,cpu 和内存的消耗;
另外 top 还可以操作进程 , 如改变优先级 (命令 r) 和关闭进程 (命令 k);
ps 主要是查看进程的,关注点在于查看需要查看的进程;
top 主要看 cpu, 内存使用情况,及占用资源最多的进程由高到低排序,关注点在于资源占用情况。
8) 压缩数据

tar -xvf 文件名;
tar -zxvf 文件名;
tar -cvzf 文件名。
9)结束进程:kill PID 或者 kill all

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值