-
1. 理解DOM和页面结构在开始获取和解析页面内容之前,我们需要理解DOM(Document Object Model)的概念。DOM是将HTML或XML文档表示为树状结构的编程接口,其中每个节点都是文档的一部分,如元素、属性或文本。1.1 DOM树结构DOM将文档表示为节点树,其中:文档节点是整个文档的根节点元素节点代表HTML元素属性节点代表HTML属性文本节点包含元素内的文本内容<!DOCTYPE html><html><head> <title>示例页面</title></head><body> <h1>欢迎</h1> <p class="intro">这是一个示例段落。</p></body></html>对应的DOM树结构:Documenthtmlheadtitle“示例页面” (文本节点)bodyh1“欢迎” (文本节点)p (class属性为"intro")“这是一个示例段落。” (文本节点)1.2 为什么需要解析页面内容解析页面内容有许多实际应用:网页抓取(Web Scraping)内容分析自动化测试浏览器扩展开发数据提取和转换2. 获取整个页面的HTML代码2.1 使用document.documentElement.outerHTML获取整个页面HTML代码的最简单方法是使用document.documentElement.outerHTML属性:const fullPageHTML = document.documentElement.outerHTML;console.log(fullPageHTML); // 输出完整的HTML文档原理:document.documentElement代表HTML文档的根元素(通常是<html>元素)outerHTML属性获取元素及其所有子元素的HTML表示2.2 使用document.documentElement.innerHTML如果只需要<html>元素内部的内容(不包括<html>标签本身),可以使用:const htmlContent = document.documentElement.innerHTML;console.log(htmlContent); // 输出<html>内部的所有内容2.3 使用document.getElementsByTagName(‘html’)[0]另一种获取整个HTML内容的方式:const htmlElement = document.getElementsByTagName('html')[0];const fullHTML = htmlElement.outerHTML;console.log(fullHTML);2.4 获取DOCTYPE声明如果需要包含DOCTYPE声明,可以组合使用:const doctype = document.doctype;const doctypeString = doctype ? `<!DOCTYPE ${doctype.name}${doctype.publicId ? ` PUBLIC "${doctype.publicId}"` : ''}${doctype.systemId ? ` "${doctype.systemId}"` : ''}>` : '';const fullDocument = doctypeString + document.documentElement.outerHTML;console.log(fullDocument);3. 解析页面内容获取HTML代码后,下一步是解析其中的内容。JavaScript提供了多种方法来选择和操作DOM元素。3.1 使用DOM选择器方法3.1.1 getElementById通过元素的ID获取单个元素:const header = document.getElementById('header');console.log(header.textContent);3.1.2 getElementsByClassName通过类名获取元素集合:const items = document.getElementsByClassName('item');Array.from(items).forEach(item => { console.log(item.textContent);});3.1.3 getElementsByTagName通过标签名获取元素集合:const paragraphs = document.getElementsByTagName('p');Array.from(paragraphs).forEach(p => { console.log(p.innerHTML);});3.1.4 querySelector和querySelectorAll使用CSS选择器语法选择元素:// 获取第一个匹配的元素const firstItem = document.querySelector('.list-item');console.log(firstItem.textContent);// 获取所有匹配的元素const allItems = document.querySelectorAll('.list-item');allItems.forEach(item => { console.log(item.textContent);});3.2 遍历DOM树3.2.1 父节点和子节点const parent = document.querySelector('.parent');const children = parent.children; // 获取所有子元素// 遍历子节点Array.from(children).forEach(child => { console.log(child.tagName);});// 获取父节点const child = document.querySelector('.child');const parentNode = child.parentNode;console.log(parentNode.tagName);3.2.2 兄弟节点const item = document.querySelector('.item');const nextSibling = item.nextElementSibling;const previousSibling = item.previousElementSibling;console.log('下一个兄弟节点:', nextSibling);console.log('上一个兄弟节点:', previousSibling);3.2.3 递归遍历整个DOM树function traverseDOM(node, depth = 0) { // 打印当前节点信息 console.log(`${' '.repeat(depth * 2)}${node.nodeName}${node.nodeValue ? `: ${node.nodeValue.trim()}` : ''}`); // 如果有子节点,递归遍历 if (node.childNodes && node.childNodes.length > 0) { Array.from(node.childNodes).forEach(child => { traverseDOM(child, depth + 1); }); }}// 从body开始遍历traverseDOM(document.body);3.3 提取元素属性和内容3.3.1 获取元素属性const link = document.querySelector('a');console.log('href:', link.getAttribute('href'));console.log('class:', link.className);console.log('id:', link.id);console.log('所有属性:', link.attributes);3.3.2 获取元素文本内容const paragraph = document.querySelector('p');console.log('textContent:', paragraph.textContent); // 包括隐藏元素的文本console.log('innerText:', paragraph.innerText); // 仅显示文本,受CSS影响console.log('innerHTML:', paragraph.innerHTML); // 包含HTML标签3.3.3 获取表单元素值const input = document.querySelector('input[type="text"]');console.log('输入值:', input.value);const checkbox = document.querySelector('input[type="checkbox"]');console.log('是否选中:', checkbox.checked);const select = document.querySelector('select');console.log('选择的值:', select.value);console.log('选择的文本:', select.options[select.selectedIndex].text);4. 高级解析技术4.1 使用XPath解析XPath提供了一种在XML/HTML文档中导航和选择节点的强大方式:// 评估XPath表达式function evaluateXPath(xpath, context = document) { const result = []; const query = document.evaluate(xpath, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < query.snapshotLength; i++) { result.push(query.snapshotItem(i)); } return result;}// 使用示例:获取所有h2标题的文本const headings = evaluateXPath('//h2');headings.forEach(h2 => { console.log(h2.textContent);});4.2 使用TreeWalker遍历DOMTreeWalker接口提供了更灵活的DOM遍历方式:const treeWalker = document.createTreeWalker( document.body, // 根节点 NodeFilter.SHOW_ELEMENT, // 只显示元素节点 { acceptNode: function(node) { return node.tagName === 'P' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }}, // 只接受<p>元素 false);const paragraphs = [];let currentNode = treeWalker.nextNode();while (currentNode) { paragraphs.push(currentNode); currentNode = treeWalker.nextNode();}console.log('找到的段落:', paragraphs);4.3 使用DOMParser解析HTML字符串如果需要解析HTML字符串而不是现有文档:const htmlString = `<html><body><h1>标题</h1><p>段落内容</p></body></html>`;const parser = new DOMParser();const doc = parser.parseFromString(htmlString, 'text/html');// 现在可以像普通DOM一样操作const title = doc.querySelector('h1');console.log(title.textContent); // 输出"标题"4.4 使用MutationObserver监听DOM变化如果需要监控DOM的变化:const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { console.log('DOM发生了变化:', mutation); if (mutation.addedNodes.length) { console.log('添加的节点:', mutation.addedNodes); } if (mutation.removedNodes.length) { console.log('移除的节点:', mutation.removedNodes); } });});// 开始观察body元素及其子元素的变化observer.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true});// 停止观察// observer.disconnect();5. 实际应用示例5.1 提取所有链接function extractAllLinks() { const links = document.querySelectorAll('a[href]'); const urls = Array.from(links).map(link => { return { text: link.textContent.trim(), href: link.getAttribute('href'), title: link.getAttribute('title') || '' }; }); console.log('页面中的所有链接:', urls); return urls;}extractAllLinks();5.2 提取文章内容function extractArticleContent() { // 尝试找到可能包含文章内容的元素 const potentialSelectors = [ 'article', '.article', '.post', '.content', 'main', '#main' ]; let articleElement = null; for (const selector of potentialSelectors) { const element = document.querySelector(selector); if (element) { articleElement = element; break; } } // 如果没有找到特定元素,尝试启发式方法 if (!articleElement) { // 查找包含多个段落的最长元素 const allElements = document.querySelectorAll('body *'); let maxLength = 0; allElements.forEach(el => { const textLength = el.textContent.trim().length; const paragraphCount = el.querySelectorAll('p').length; if (textLength > maxLength && paragraphCount > 1) { maxLength = textLength; articleElement = el; } }); } if (articleElement) { const title = document.querySelector('h1') || document.querySelector('title') || { textContent: '无标题' }; const paragraphs = Array.from(articleElement.querySelectorAll('p')) .map(p => p.textContent.trim()) .filter(text => text.length > 0); const images = Array.from(articleElement.querySelectorAll('img')) .map(img => img.getAttribute('src')); return { title: title.textContent.trim(), paragraphs, images }; } return null;}console.log('提取的文章内容:', extractArticleContent());5.3 提取表格数据function extractTableData() { const tables = document.querySelectorAll('table'); const tableData = []; tables.forEach((table, index) => { const rows = table.querySelectorAll('tr'); const data = []; rows.forEach(row => { const cells = row.querySelectorAll('td, th'); const rowData = Array.from(cells).map(cell => cell.textContent.trim()); data.push(rowData); }); tableData.push({ tableIndex: index + 1, rows: data }); }); console.log('提取的表格数据:', tableData); return tableData;}extractTableData();5.4 提取元数据function extractMetaData() { const metaTags = document.querySelectorAll('meta'); const metadata = {}; metaTags.forEach(tag => { const name = tag.getAttribute('name') || tag.getAttribute('property') || tag.getAttribute('itemprop'); const content = tag.getAttribute('content'); if (name && content) { metadata[name] = content; } }); // 获取标题 metadata.title = document.title; // 获取描述(优先从meta标签获取) if (!metadata.description) { const firstParagraph = document.querySelector('p'); if (firstParagraph) { metadata.description = firstParagraph.textContent.trim().substring(0, 150) + '...'; } } // 获取关键词 if (!metadata.keywords) { metadata.keywords = []; } else if (typeof metadata.keywords === 'string') { metadata.keywords = metadata.keywords.split(',').map(k => k.trim()); } console.log('页面元数据:', metadata); return metadata;}extractMetaData();6. 处理动态内容现代网页经常使用JavaScript动态加载内容,这给内容提取带来了挑战。6.1 检测动态加载的内容// 使用MutationObserver检测动态加载的内容function watchForDynamicContent(callback) { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { callback(mutation.addedNodes); } }); }); observer.observe(document.body, { childList: true, subtree: true }); return observer;}// 示例:检测新加载的内容并提取其中的链接const dynamicLinks = new Set();const observer = watchForDynamicContent(nodes => { nodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const links = node.querySelectorAll('a[href]'); links.forEach(link => { const href = link.getAttribute('href'); if (!dynamicLinks.has(href)) { dynamicLinks.add(href); console.log('发现新链接:', href); } }); } });});// 停止观察// observer.disconnect();6.2 等待特定元素出现function waitForElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`等待元素 "${selector}" 超时`)); }, timeout); });}// 使用示例waitForElement('.dynamic-content') .then(element => { console.log('元素已加载:', element); }) .catch(error => { console.error(error); });6.3 模拟滚动以加载更多内容async function scrollToLoadAllContent() { let lastHeight = document.body.scrollHeight; let attempts = 0; const maxAttempts = 10; while (attempts < maxAttempts) { // 滚动到底部 window.scrollTo(0, document.body.scrollHeight); // 等待内容加载 await new Promise(resolve => setTimeout(resolve, 2000)); // 检查高度是否变化 const newHeight = document.body.scrollHeight; if (newHeight === lastHeight) { break; } lastHeight = newHeight; attempts++; } console.log('完成滚动,最终高度:', lastHeight);}// 使用示例scrollToLoadAllContent().then(() => { console.log('所有内容已加载(或达到最大尝试次数)');});7. 性能优化和最佳实践7.1 批量操作减少重绘// 不推荐的方式(每次循环都会导致重绘)const items = document.querySelectorAll('.item');items.forEach(item => { item.style.color = 'red';});// 推荐的方式(使用文档片段批量操作)const fragment = document.createDocumentFragment();const newItems = Array(10).fill().map((_, i) => { const div = document.createElement('div'); div.className = 'item'; div.textContent = `项目 ${i + 1}`; fragment.appendChild(div); return div;});document.body.appendChild(fragment);7.2 使用事件委托提高性能// 不推荐的方式(为每个元素添加事件监听器)document.querySelectorAll('.clickable-item').forEach(item => { item.addEventListener('click', handleClick);});// 推荐的方式(事件委托)document.body.addEventListener('click', event => { if (event.target.closest('.clickable-item')) { handleClick(event); }});function handleClick(event) { console.log('点击的项目:', event.target);}7.3 缓存DOM查询结果// 不推荐的方式(多次查询相同的元素)function updateElements() { document.querySelector('.item').style.color = 'red'; document.querySelector('.item').style.fontSize = '16px'; document.querySelector('.item').textContent = '更新后的文本';}// 推荐的方式(缓存查询结果)function updateElementsOptimized() { const item = document.querySelector('.item'); item.style.color = 'red'; item.style.fontSize = '16px'; item.textContent = '更新后的文本';}7.4 使用更高效的选择器// 不高效的选择器(过于通用)const allDivs = document.querySelectorAll('div div div');// 更高效的选择器(更具体)const specificDivs = document.querySelectorAll('.container > .wrapper > .content');8. 安全考虑8.1 防止XSS攻击当处理动态内容时,要注意防范XSS(跨站脚本)攻击:// 不安全的方式(直接插入HTML)function unsafeInsert(content) { document.querySelector('.output').innerHTML = content;}// 安全的方式(使用textContent或DOMPurify)function safeInsert(content) { // 方法1:仅插入文本 document.querySelector('.output').textContent = content; // 方法2:使用DOMPurify清理HTML // const clean = DOMPurify.sanitize(content); // document.querySelector('.output').innerHTML = clean;}8.2 处理用户生成的内容function sanitizeUserInput(input) { // 移除脚本标签 let sanitized = input.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); // 移除危险的属性 sanitized = sanitized.replace(/\son\w+="[^"]*"/g, ''); // 其他清理逻辑... return sanitized;}const userInput = '<script>alert("XSS")</script><img src="x" onerror="alert(1)">';console.log('清理后的输入:', sanitizeUserInput(userInput));9. 跨域限制和解决方案9.1 同源策略限制浏览器出于安全考虑实施了同源策略,限制了从不同源(协议、域名、端口)加载和操作内容的能力。9.2 使用CORS如果目标服务器支持CORS(跨源资源共享),可以直接请求:fetch('https://api.example.com/data', { method: 'GET', mode: 'cors', headers: { 'Content-Type': 'application/json' }}).then(response => response.json()).then(data => console.log(data)).catch(error => console.error('错误:', error));9.3 使用代理服务器对于不支持CORS的网站,可以通过自己的服务器代理请求:// 前端代码fetch('/proxy?url=' + encodeURIComponent('https://example.com')) .then(response => response.text()) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 解析文档... });// 服务器端(Node.js示例)/*app.get('/proxy', async (req, res) => { const { url } = req.query; try { const response = await axios.get(url); res.send(response.data); } catch (error) { res.status(500).send('代理请求失败'); }});*/9.4 浏览器扩展解决方案如果是开发浏览器扩展,可以使用chrome.webRequest API绕过某些限制:// 在manifest.json中声明权限/*"permissions": [ "webRequest", "webRequestBlocking", "<all_urls>"]*/// 在background.js中/*chrome.webRequest.onBeforeSendHeaders.addListener( details => { // 修改请求头 details.requestHeaders.push({ name: 'Origin', value: 'https://your-extension-id.chromiumapp.org' }); return { requestHeaders: details.requestHeaders }; }, { urls: ['<all_urls>'] }, ['blocking', 'requestHeaders']);*/10. 完整的页面解析工具示例下面是一个完整的示例,展示如何构建一个功能丰富的页面解析工具:class PageParser { constructor() { this.parsedData = { metadata: {}, structure: {}, content: {}, resources: {} }; } // 解析页面元数据 parseMetadata() { // 标题 this.parsedData.metadata.title = document.title; // meta标签 this.parsedData.metadata.metaTags = {}; document.querySelectorAll('meta').forEach(tag => { const name = tag.getAttribute('name') || tag.getAttribute('property') || tag.getAttribute('itemprop'); if (name) { this.parsedData.metadata.metaTags[name] = tag.getAttribute('content'); } }); // 链接标签 this.parsedData.metadata.links = []; document.querySelectorAll('link').forEach(link => { this.parsedData.metadata.links.push({ rel: link.getAttribute('rel'), href: link.getAttribute('href'), type: link.getAttribute('type') }); }); return this; } // 分析页面结构 analyzeStructure() { // 统计各类元素数量 this.parsedData.structure.elementCounts = {}; const allElements = document.querySelectorAll('*'); Array.from(allElements).forEach(el => { const tag = el.tagName.toLowerCase(); this.parsedData.structure.elementCounts[tag] = (this.parsedData.structure.elementCounts[tag] || 0) + 1; }); // 获取主要内容区域 this.parsedData.structure.mainContent = this.findMainContent(); return this; } // 查找主要内容区域 findMainContent() { const contentSelectors = [ 'main', 'article', '.main-content', '.content', '#content', '.article', '.post' ]; for (const selector of contentSelectors) { const element = document.querySelector(selector); if (element) { return { selector, textLength: element.textContent.length, paragraphCount: element.querySelectorAll('p').length }; } } // 启发式方法:查找包含最多文本的元素 let maxLength = 0; let mainElement = null; document.querySelectorAll('body > div, body > section').forEach(el => { const length = el.textContent.length; if (length > maxLength) { maxLength = length; mainElement = el; } }); return mainElement ? { selector: this.generateSelector(mainElement), textLength: mainElement.textContent.length, paragraphCount: mainElement.querySelectorAll('p').length } : null; } // 生成元素选择器 generateSelector(element) { if (element.id) { return `#${element.id}`; } const path = []; let current = element; while (current && current !== document.body) { let selector = current.tagName.toLowerCase(); if (current.className && typeof current.className === 'string') { const classes = current.className.split(/\s+/).filter(c => c); if (classes.length) { selector += `.${classes.join('.')}`; } } // 如果有兄弟元素,添加:nth-child const siblings = Array.from(current.parentNode.children); const index = siblings.indexOf(current); if (siblings.length > 1) { selector += `:nth-child(${index + 1})`; } path.unshift(selector); current = current.parentNode; } return path.join(' > '); } // 提取页面内容 extractContent() { // 提取所有文本段落 this.parsedData.content.paragraphs = Array.from(document.querySelectorAll('p')) .map(p => p.textContent.trim()) .filter(text => text.length > 0); // 提取所有标题 this.parsedData.content.headings = {}; ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach(tag => { this.parsedData.content.headings[tag] = Array.from(document.querySelectorAll(tag)) .map(el => el.textContent.trim()); }); // 提取图片 this.parsedData.content.images = Array.from(document.querySelectorAll('img')) .map(img => ({ src: img.getAttribute('src'), alt: img.getAttribute('alt') || '', width: img.width, height: img.height })); return this; } // 收集页面资源 collectResources() { // 脚本 this.parsedData.resources.scripts = Array.from(document.querySelectorAll('script[src]')) .map(script => script.getAttribute('src')); // 样式表 this.parsedData.resources.stylesheets = Array.from(document.querySelectorAll('link[rel="stylesheet"]')) .map(link => link.getAttribute('href')); // 图片 this.parsedData.resources.images = Array.from(document.querySelectorAll('img[src]')) .map(img => img.getAttribute('src')); // 外部链接 this.parsedData.resources.links = Array.from(document.querySelectorAll('a[href]')) .filter(a => { const href = a.getAttribute('href'); return href && !href.startsWith('#') && !href.startsWith('javascript:'); }) .map(a => a.getAttribute('href')); return this; } // 获取解析结果 getResult() { return this.parsedData; } // 静态方法:完整解析页面 static parseFullPage() { return new PageParser() .parseMetadata() .analyzeStructure() .extractContent() .collectResources() .getResult(); }}// 使用示例document.addEventListener('DOMContentLoaded', () => { const pageData = PageParser.parseFullPage(); console.log('完整页面分析结果:', pageData); // 可以将结果发送到服务器或保存 // fetch('/api/save-analysis', { // method: 'POST', // body: JSON.stringify(pageData) // });});11. 总结本文详细介绍了如何使用JavaScript获取和解析页面内容,涵盖了从基础到高级的各种技术。我们学习了:获取页面HTML:使用outerHTML、innerHTML等方法获取完整或部分的HTML代码DOM遍历和选择:使用各种选择器方法和遍历技术定位特定元素内容提取:从元素中提取文本、属性和结构化数据高级技术:XPath、TreeWalker、MutationObserver等高级API的使用动态内容处理:监控和等待动态加载的内容性能优化:批量操作、事件委托等提高性能的技术安全考虑:防范XSS攻击和正确处理用户输入跨域限制:理解和解决同源策略带来的限制完整工具实现:构建一个功能全面的页面解析工具通过这些技术,你可以构建强大的网页抓取工具、内容分析系统或浏览器扩展,满足各种实际应用需求。记住在实际应用中要考虑性能、安全和合法性,确保你的代码既高效又负责任。————————————————原文链接:https://blog.csdn.net/sixpp/article/details/146491098
-
今天学习到了Java中的String,String是Java中一个非常重要的类,在我们做字符串操作的时候,需要使用到String。一、什么是StringString是JDK提供的一个类,是引用类型,并不是基本的数据类型,String用于字符串操作,在之前学习c语言的时候,对于一些字符串,会初始化字符数组表示。char str [] = {'a', 'b', 'c'};在Java中,String像是一个很大的char数组,我们在定义字符串的时候,不需要再去像上方代码一样去定义,而是直接可以使用String去定义。String str = "abc";String就像是很大的char数组,但相比于char数组而言,String可以做字符串拼接操作,而char数组并不能直接去做字符串的拼接,如下代码。String str = "abc" + "def";二、如何定义一个String既然知道String是一个类,那么类一定有他的初始化方法,在Java中,String的初始化有以下几种常用的方式。这是官方给出的一些介绍。 1. 用双引号定义String字符串,可以直接通过双引号来定义,把需要的字符串内容用双引号包裹,可以直接对String类型的对象赋值。public class StringDemo { public static void main(String[] args) { String str = "abc"; }}2. 通过构造函数定义String作为一个类,可以通过new关键字初始化,在Java中可以通过构造函数对String类型的对象赋值。public class StringDemo { public static void main(String[] args) { String str = new String("abc"); System.out.println(str); }}这是一种基本的构造字符串的方式,除此之外还有一些其他的构造方式。通过无参构造函数初始化这样的方式初始化的String的值为空,也就是什么都没有。public String() { this.value = "".value; this.coder = "".coder; }通过char数组进行初始化String可以传入一个char数组进行初始化,String会拼接char数组当中的所有字符。public String(char value[]) { this(value, 0, value.length, null); }带位移的方式通过char数组进行初始化这样的方式和上边的方式其实是一样的,只不过在初始化字符串的时候,会根据传入的offset作为char数组开始拼接的起始索引,并且拼接上count个字符。public String(char value[], int offset, int count) { this(value, offset, count, rangeCheck(value, offset, count)); }除了以上几种常用的方式外,还有一些其他的方式,如通过int数组进行初始化,通过byte数组进行初始化,大家感兴趣的话可以自己研究一下。 三、String中的一些常用方法了解到什么是String,下面就要理解一些String中常用的方法。1 字符串比较字符串之间的比较又分为很多不同的方法,下边是一些常用的字符串比较方法。 1.1 字符串使用 ==字符串使用 == 操作,其实是一个有坑的点,一般不用,这里不在多讲,有兴趣的可以尝试一下以下代码。 1.2 字符串使用equals()字符串的比较,使用这个方法比较多一点。 1.3 使用 equalsIgnoreCase()这个方法相比于普通的equals方法的区别是,这个方法是忽略大小写的。 除此之外,字符串比较还有这两个方法。1.4 cpmpareTo和compareToIgnoreCase这两个方法和equals方法的区别是,equals返回的是boolean类型的变量,而compare方法返回的是int类型的变量。具体比较方法如下:两个字符串按照单个字符从前向后作比较,遇到不同的字符,返回两个字符的差值如果两个字符比较完成了,没有发现不同的字符,返回两个字符串的长度差。2 字符串大小写转换 String中还提供了一些字符串大小写转换的方法。 2.1 toUpperCase()这个方法就是把字符串全部转换为大写。 2.2 toLowerCase()这个方法把字符串全部转换为小写 3 字符串长度获取3.1 length()字符串长度的获取通过以下方法获取。 4 判断是否包含一段字符串4.1 contains()String是有子字符串的概念的,比如我们要查看某个字符串中是否包含一小段的字符串,我们可以用以下方法。 5 字符串切割和拼接 5.1 split()字符串的切割和拼接是非常实用的方法,现在我们有这样的一个字符串。String students = "zhangsan,lisi,wangwu,liuliu";我们想要把这些姓名拆分出来,就需要用到下边的方法。 对于字符串切割,我们需要传入一个作为分割的字符,在上方的代码当中传入的是一个",",当然在具体应用的时候,还要根据具体的场景做分析。5.2 join()除了字符串分割外,当然也有字符串拼接的操作,如果我们想把studentArr中的学生姓名用横杠拼接起来,像这样zhangsan-lisi-wangwu-liuliu需要以下代码完成,这里只介绍基本的使用。 6 字符串寻找字串起始位置我们可以判断字串是否存在,也可以获取子串在字符串的起始索引。 6.1 indexOf()看下方代码,我们试图在字符串中寻找有没有值为"lisi"的字符串,我们调用indexOf方法就可以,indexOf方法有两种,一种是直接传匹配字串,另外一种是传入匹配子串的同时传入开始匹配的起始下边,比如我们从索引10开始寻找,因为lisi的开始索引为9,当我们把开始匹配的下标放到10的时候,就没有办法在匹配到"lisi"了。 6.2 lastIndexOf() 和indexOf()是一样的,不同的是,lastIndexOf()是判断的结尾,是从后往前找的。7 获取字串7.1 substring()字串可以判断存不存在,可以获取起始下标,当然也可以获取子串。通过起始下标和结束下标来截取子串。8 字符串替换 replace:有两个重载形式,replace(char oldChar, char newChar)用于字符替换,replace(CharSequence target, CharSequence replacement) 可用于字符串替换。这里的CharSequence是字符串序列,简单理解就是字符串 ,该方法不会将参数解析为正则表达式。replaceAll:方法签名为replaceAll(String regex, String replacement),参数regex要求是一个正则表达式字符串,它会按照正则表达式规则去匹配字符串中的子串并替换 。如果传入的不是正则表达式,也会当作普通字符串处理。replaceFirst:方法签名为replaceFirst(String regex, String replacement) ,和replaceAll一样,第一个参数regex也是基于正则表达式的,不过它只替换第一次匹配到的子串。当传入非正则表达式的普通字符串时,也能进行字符串替换操作。 对于一些不想要的字符,也是可以替换的,拿上方的这个字符串来说。String students = "zhangsan,lisi,wangwu,liuliu";如果不想要逗号了,想要用横线分割,下方代码可以实现。8.1 replace()8.2 replaceAll()与replace()不同的是,replaceAll()是可以传入正则表达式的,这里不在讲正则表达式,以下的方式也是可以替换的。 以上就是一些常用的String的使用方法。————————————————原文链接:https://blog.csdn.net/2201_76027234/article/details/146368113
-
1. 关键字定义:被Java语言赋予了特殊含义,用做专门用途的字符串(或单词)HelloWorld案例中,出现的关键字有 class、public 、 static 、 void 等,这些单词已经被Java定义好了。特点:全部关键字都是小写字母。关键字比较多,不需要死记硬背,学到哪里记到哪里即可。 说明: 关键字一共50个,其中const和goto是保留字(reserved word)。true,false,null不在其中,它们看起来像关键字,其实是字面量,表示特殊的布尔值和空值。2. 标识符Java中变量、方法、类等要素命名时使用的字符序列,称为标识符。 技巧:凡是自己可以起名字的地方都叫标识符。 标识符的命名规则(必须遵守的硬性规定): 由26个英文字母大小写,0-9 ,_或 $ 组成数字不可以开头。不可以使用关键字和保留字,但能包含关键字和保留字。Java中严格区分大小写,长度无限制。标识符不能包含空格。 3. 变量3.1 初识变量变量的概念: 内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化 变量的构成包含三个要素:数据类型、变量名、存储的值 Java中变量声明的格式:数据类型 变量名 = 变量值 变量的作用:用于在内存中保存数据。 使用变量注意: Java中每个变量必须先声明,后使用。使用变量名来访问这块区域的数据。变量的作用域:其定义所在的一对{ }内。变量只有在其作用域内才有效。出了作用域,变量不可以再被调用。同一个作用域内,不能定义重名的变量。3.2 Java中变量的数据类型Java中变量的数据类型分为两大类: 基本数据类型:包括 整数类型、浮点数类型、字符类型、布尔类型。 引用数据类型:包括数组、 类、接口、枚举、注解、记录。 3.3 变量的使用3.3.1 步骤1:变量的声明格式:数据类型 变量名;1//例如://存储一个整数类型的年龄int age; //存储一个小数类型的体重double weight; //存储一个单字符类型的性别 char gender; //存储一个布尔类型的婚姻状态boolean marry; //存储一个字符串类型的姓名String name; //声明多个同类型的变量int a,b,c; //表示a,b,c三个变量都是int类型。 注意:变量的数据类型可以是基本数据类型,也可以是引用数据类型。 3.3.2 步骤2:变量的赋值给变量赋值,就是把“值”存到该变量代表的内存空间中。同时,给变量赋的值类型必须与变量声明的类型一致或兼容。 变量赋值的语法格式: 变量名 = 值; 举例1:可以使用合适类型的常量值给已经声明的变量赋值 age = 18;weight = 109;gender = '女'; 举例2:可以使用其他变量或者表达式给变量赋值 int m = 1;int n = m; int x = 1;int y = 2;int z = 2 * x + y; 3:变量可以反复赋值 //先声明,后初始化char gender;gender = '女'; //给变量重新赋值,修改gender变量的值gender = '男';System.out.println("gender = " + gender);//gender = 男 举例4:也可以将变量的声明和赋值一并执行 boolean isBeauty = true;String name = "迪丽热巴"; 内存结构如图: 4. 基本数据类型介绍4.1 整数类型:byte、short、int、longJava各整数类型有固定的表数范围和字段长度,不受具体操作系统的影响,以保证Java程序的可移植性。 定义long类型的变量,赋值时需要以"l"或"L"作为后缀。 Java程序中变量通常声明为int型,除非不足以表示较大的数,才使用long。 Java的整型常量默认为 int 型。 4.1.1 补充:计算机存储单位**字节(Byte):**是计算机用于计量存储容量的基本单位,一个字节等于8 bit。 **位(bit):**是数据存储的最小单位。二进制数系统中,每个0或1就是一个位,叫做bit(比特),其中8 bit 就称为一个字节(Byte)。 转换关系: 8 bit = 1 Byte1024 Byte = 1 KB1024 KB = 1 MB1024 MB = 1 GB1024 GB = 1 TB4.2 浮点类型:float、double与整数类型类似,Java 浮点类型也有固定的表数范围和字段长度,不受具体操作系统的影响。 浮点型常量有两种表示形式:十进制数形式。如:5.12 512.0f .512 (必须有小数点)科学计数法形式。如:5.12e2 512E2 100E-2float:单精度,尾数可以精确到7位有效数字。很多情况下,精度很难满足需求。double:双精度,精度是float的两倍。通常采用此类型。定义float类型的变量,赋值时需要以"f"或"F"作为后缀。Java 的浮点型常量默认为double型。4.2.1 关于浮点型精度的说明并不是所有的小数都能可以精确的用二进制浮点数表示。二进制浮点数不能精确的表示0.1、0.01、0.001这样10的负次幂。 浮点类型float、double的数据不适合在不容许舍入误差的金融计算领域。如果需要精确数字计算或保留指定位数的精度,需要使用BigDecimal类。 测试用例: //测试1:(解释见章末企业真题:为什么0.1 + 0.2不等于0.3)System.out.println(0.1 + 0.2);//0.30000000000000004 //测试2:float ff1 = 123123123f;float ff2 = ff1 + 1;System.out.println(ff1);System.out.println(ff2);System.out.println(ff1 == ff2); 4.3 字符类型:charchar 型数据用来表示通常意义上“字符”(占2字节) Java中的所有字符都使用Unicode编码,故一个字符可以存储一个字母,一个汉字,或其他书面语的一个字符。 字符型变量的三种表现形式: **形式1:**使用单引号(’ ')括起来的单个字符。 例如:char c1 = ‘a’; char c2 = ‘中’; char c3 = ‘9’; **形式2:**直接使用 Unicode值来表示字符型常量:‘\uXXXX’。其中,XXXX代表一个十六进制整数。 例如:\u0023 表示 ‘#’。 **形式3:**Java中还允许使用转义字符‘\’来将其后的字符转变为特殊字符型常量。 例如:char c3 = ‘\n’; // '\n’表示换行符 转义字符 说明 Unicode表示方式\n 换行符 \u000a\t 制表符 \u0009\" 双引号 \u0022\' 单引号 \u0027\\ 反斜线 \u005c\b 退格符 \u0008\r 回车符 \u000dchar类型是可以进行运算的。因为它都对应有Unicode码,可以看做是一个数值。 4.4 布尔类型:booleanboolean 类型用来判断逻辑条件,一般用于流程控制语句中: if条件控制语句;while循环控制语句;for循环控制语句;do-while循环控制语句;boolean类型数据只有两个值:true、false,无其它。 不可以使用0或非 0 的整数替代false和true,这点和C语言不同。拓展:Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达所操作的boolean值,在编译之后都使用java虚拟机中的int数据类型来代替:true用1表示,false用0表示。——《java虚拟机规范 8版》举例: boolean isFlag = true; if(isFlag){ //true分支}else{ //false分支} 经验之谈: Less is More!建议不要这样写:if ( isFlag = = true ),只有新手才如此。关键也很容易写错成if(isFlag = true),这样就变成赋值isFlag为true而不是判断!老鸟的写法是if (isFlag)或者if ( !isFlag)。 5. 基本数据类型变量间运算规则在Java程序中,不同的基本数据类型(只有7种,不包含boolean类型)变量的值经常需要进行相互转换。 转换的方式有两种:自动类型提升和强制类型转换。 5.1 自动类型提升规则:将取值范围小(或容量小)的类型自动提升为取值范围大(或容量大)的类型 。 基本数据类型的转换规则如图所示: (1)当把存储范围小的值(常量值、变量的值、表达式计算的结果值)赋值给了存储范围大的变量时 int i = 'A';//char自动升级为int,其实就是把字符的编码值赋值给i变量了double d = 10;//int自动升级为doublelong num = 1234567; //右边的整数常量值如果在int范围呢,编译和运行都可以通过,这里涉及到数据类型转换 //byte bigB = 130;//错误,右边的整数常量值超过byte范围long bigNum = 12345678912L;//右边的整数常量值如果超过int范围,必须加L,显式表示long类型。否则编译不通过 (2)当存储范围小的数据类型与存储范围大的数据类型变量一起混合运算时,会按照其中最大的类型运算。 int i = 1;byte b = 1;double d = 1.0; double sum = i + b + d;//混合运算,升级为double (3)当byte,short,char数据类型的变量进行算术运算时,按照int类型处理。 byte b1 = 1;byte b2 = 2;byte b3 = b1 + b2;//编译报错,b1 + b2自动升级为int char c1 = '0';char c2 = 'A';int i = c1 + c2;//至少需要使用int类型来接收System.out.println(c1 + c2);//113 5.2 强制类型转换将3.14 赋值到int 类型变量会发生什么?产生编译失败,肯定无法赋值。 int i = 3.14; // 编译报错 想要赋值成功,只有通过强制类型转换,将double 类型强制转换成int 类型才能赋值。 规则:将取值范围大(或容量大)的类型强制转换成取值范围小(或容量小)的类型。 自动类型提升是Java自动执行的,而强制类型转换是自动类型提升的逆运算,需要我们自己手动执行。 转换格式: 数据类型1 变量名 = (数据类型1)被强转数据值; //()中的数据类型必须<=变量值的数据类型 (1)当把存储范围大的值(常量值、变量的值、表达式计算的结果值)强制转换为存储范围小的变量时,可能会损失精度或溢出。 int i = (int)3.14;//损失精度 double d = 1.2;int num = (int)d;//损失精度 int i = 200;byte b = (byte)i;//溢出 (2)当某个值想要提升数据类型时,也可以使用强制类型转换。这种情况的强制类型转换是没有风险的,通常省略。 int i = 1;int j = 2;double bigger = (double)(i/j); (3)声明long类型变量时,可以出现省略后缀的情况。float则不同。 long l1 = 123L;long l2 = 123;//如何理解呢? 此时可以看做是int类型的123自动类型提升为long类型 //long l3 = 123123123123; //报错,因为123123123123超出了int的范围。long l4 = 123123123123L; //float f1 = 12.3; //报错,因为12.3看做是double,不能自动转换为float类型float f2 = 12.3F;float f3 = (float)12.3; 6 基本数据类型与String的运算6.1 字符串类型:StringString不是基本数据类型,属于引用数据类型使用一对""来表示一个字符串,内部可以包含0个、1个或多个字符。声明方式与基本数据类型类似。例如:String str = “尚硅谷”;6.2 运算规则1、任意八种基本数据类型的数据与String类型只能进行连接“+”运算,且结果一定也是String类型 System.out.println("" + 1 + 2);//12 int num = 10;boolean b1 = true;String s1 = "abc"; String s2 = s1 + num + b1;System.out.println(s2);//abc10true //String s3 = num + b1 + s1;//编译不通过,因为int类型不能与boolean运算String s4 = num + (b1 + s1);//编译通过 2、String类型不能通过强制类型()转换,转为其他的类型 String str = "123";int num = (int)str;//错误的 int num = Integer.parseInt(str);//正确的,后面才能讲到,借助包装类的方法才能转 7. 运算符运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。 运算符的分类: 按照功能分为:算术运算符、赋值运算符、比较(或关系)运算符、逻辑运算符、位运算符、条件运算符、Lambda运算符分类 运算符算术运算符(7个) +、-、*、/、%、++、–赋值运算符(12个) =、+=、-=、*=、/=、%=、>>=、<<=、>>>=、&=、|=、^=等比较(或关系)运算符(6个) >、>=、<、<=、==、!=逻辑运算符(6个) &、|、^、!、&&、||位运算符(7个) &、|、^、~、<<、>>、>>>条件运算符(1个) (条件表达式)?结果1:结果2Lambda运算符(1个) ->(第18章时讲解)按照操作数个数分为:一元运算符(单目运算符)、二元运算符(双目运算符)、三元运算符 (三目运算符)分类 运算符一元运算符(单目运算符) 正号(+)、负号(-)、++、–、!、~二元运算符(双目运算符) 除了一元和三元运算符剩下的都是二元运算符三元运算符 (三目运算符) (条件表达式)?结果1:结果27.1 算术运算符7.1.1 基本语法 举例1:加减乘除模 public class ArithmeticTest1 {public static void main(String[] args) {int a = 3;int b = 4; System.out.println(a + b);// 7System.out.println(a - b);// -1System.out.println(a * b);// 12System.out.println(a / b);// 计算机结果是0,为什么不是0.75呢?System.out.println(a % b);// 3 //结果与被模数符号相同 System.out.println(5%2);//1System.out.println(5%-2);//1System.out.println(-5%2);//-1System.out.println(-5%-2);//-1//商*除数 + 余数 = 被除数//5%-2 ==>商是-2,余数时1 (-2)*(-2)+1 = 5//-5%2 ==>商是-2,余数是-1 (-2)*2+(-1) = -4-1=-5}} 举例2:“+”号的两种用法 第一种:对于+两边都是数值的话,+就是加法的意思第二种:对于+两边至少有一边是字符串的话,+就是拼接的意思public class ArithmeticTest2 {public static void main(String[] args) {// 字符串类型的变量基本使用// 数据类型 变量名称 = 数据值;String str1 = "Hello";System.out.println(str1); // Hello System.out.println("Hello" + "World"); // HelloWorld String str2 = "Java";// String + int --> StringSystem.out.println(str2 + 520); // Java520// String + int + int// String + int// StringSystem.out.println(str2 + 5 + 20); // Java520}} 举例3:自加自减运算 理解:++ 运算,表示自增1。同理,-- 运算,表示自减1,用法与++ 一致。 1、单独使用 变量在单独运算的时候,变量前++和变量后++,是没有区别的。变量前++ :例如 ++a 。变量后++ :例如 a++ 。public class ArithmeticTest3 {public static void main(String[] args) {// 定义一个int类型的变量aint a = 3;//++a;a++; // 无论是变量前++还是变量后++,结果都是4System.out.println(a);}} 2、复合使用 和其他变量放在一起使用或者和输出语句放在一起使用,前++和后++就产生了不同。变量前++ :变量先自增1,然后再运算。变量后++ :变量先运算,然后再自增1。public class ArithmeticTest4 {public static void main(String[] args) {// 其他变量放在一起使用int x = 3;//int y = ++x; // y的值是4,x的值是4,int y = x++; // y的值是3,x的值是4 System.out.println(x);System.out.println(y);System.out.println("=========="); // 和输出语句一起int z = 5;//System.out.println(++z);// 输出结果是6,z的值也是6System.out.println(z++);// 输出结果是5,z的值是6System.out.println(z); } } 7.2 赋值运算符7.2.1 基本语法符号:= 当“=”两侧数据类型不一致时,可以使用自动类型转换或使用强制类型转换原则进行处理。支持连续赋值。扩展赋值运算符: +=、 -=、*=、 /=、%= 赋值运算符 符号解释+= 将符号左边的值和右边的值进行相加操作,最后将结果赋值给左边的变量-= 将符号左边的值和右边的值进行相减操作,最后将结果赋值给左边的变量*= 将符号左边的值和右边的值进行相乘操作,最后将结果赋值给左边的变量/= 将符号左边的值和右边的值进行相除操作,最后将结果赋值给左边的变量%= 将符号左边的值和右边的值进行取余操作,最后将结果赋值给左边的变量public class SetValueTest1 {public static void main(String[] args) {int i1 = 10;long l1 = i1; //自动类型转换 byte bb1 = (byte)i1; //强制类型转换 int i2 = i1; //连续赋值的测试//以前的写法int a1 = 10;int b1 = 10; //连续赋值的写法int a2,b2;a2 = b2 = 10; int a3 = 10,b3 = 20; //举例说明+= -= *= /= %= int m1 = 10;m1 += 5; //类似于 m1 = m1 + 5的操作,但不等同于。System.out.println(m1);//15 //练习1:开发中,如何实现一个变量+2的操作呢?// += 的操作不会改变变量本身的数据类型。其他拓展的运算符也如此。//写法1:推荐short s1 = 10;s1 += 2; //编译通过,因为在得到int类型的结果后,JVM自动完成一步强制类型转换,将int类型强转成shortSystem.out.println(s1);//12//写法2:short s2 = 10;//s2 = s2 + 2;//编译报错,因为将int类型的结果赋值给short类型的变量s时,可能损失精度s2 = (short)(s2 + 2);System.out.println(s2); //练习2:开发中,如何实现一个变量+1的操作呢?//写法1:推荐int num1 = 10;num1++;System.out.println(num1); //写法2:int num2 = 10;num2 += 1;System.out.println(num2); //写法3:int num3 = 10;num3 = num3 + 1;System.out.println(num3); }} 7.3 比较(关系)运算符 比较运算符的结果都是boolean型,也就是要么是true,要么是false。 > < >= <= :只适用于基本数据类型(除boolean类型之外) == != :适用于基本数据类型和引用数据类型 比较运算符“==”不能误写成“=” 举例: class CompareTest {public static void main(String[] args) {int i1 = 10;int i2 = 20; System.out.println(i1 == i2);//falseSystem.out.println(i1 != i2);//trueSystem.out.println(i1 >= i2);//false int m = 10;int n = 20;System.out.println(m == n);//falseSystem.out.println(m = n);//20 boolean b1 = false;boolean b2 = true;System.out.println(b1 == b2);//falseSystem.out.println(b1 = b2);//true}} 7.4 逻辑运算符7.4.1 基本语法 逻辑运算符,操作的都是boolean类型的变量或常量,而且运算得结果也是boolean类型的值。 运算符说明: & 和 &&:表示"且"关系,当符号左右两边布尔值都是true时,结果才能为true。否则,为false。| 和 || :表示"或"关系,当符号两边布尔值有一边为true时,结果为true。当两边都为false时,结果为false! :表示"非"关系,当变量布尔值为true时,结果为false。当变量布尔值为false时,结果为true。^ :当符号左右两边布尔值不同时,结果为true。当两边布尔值相同时,结果为false。理解:异或,追求的是“异”!逻辑运算符用于连接布尔型表达式,在Java中不可以写成 3 < x < 6,应该写成x > 3 & x < 6 。 区分“&”和“&&”: 相同点:如果符号左边是true,则二者都执行符号右边的操作 不同点:& : 如果符号左边是false,则继续执行符号右边的操作 && :如果符号左边是false,则不再继续执行符号右边的操作 建议:开发中,推荐使用 &&区分“|”和“||”: 相同点:如果符号左边是false,则二者都执行符号右边的操作 不同点:| : 如果符号左边是true,则继续执行符号右边的操作 || :如果符号左边是true,则不再继续执行符号右边的操作 建议:开发中,推荐使用 || 代码举例: public class LoginTest {public static void main(String[] args) {int a = 3;int b = 4;int c = 5; // & 与,且;有false则falseSystem.out.println((a > b) & (a > c)); System.out.println((a > b) & (a < c)); System.out.println((a < b) & (a > c)); System.out.println((a < b) & (a < c)); System.out.println("===============");// | 或;有true则trueSystem.out.println((a > b) | (a > c)); System.out.println((a > b) | (a < c)); System.out.println((a < b) | (a > c));System.out.println((a < b) | (a < c));System.out.println("===============");// ^ 异或;相同为false,不同为trueSystem.out.println((a > b) ^ (a > c));System.out.println((a > b) ^ (a < c)); System.out.println((a < b) ^ (a > c)); System.out.println((a < b) ^ (a < c)); System.out.println("===============");// ! 非;非false则true,非true则falseSystem.out.println(!false);System.out.println(!true); //&和&&的区别 System.out.println((a > b) & (a++ > c)); System.out.println("a = " + a); System.out.println((a > b) && (a++ > c)); System.out.println("a = " + a); System.out.println((a == b) && (a++ > c)); System.out.println("a = " + a); //|和||的区别 System.out.println((a > b) | (a++ > c)); System.out.println("a = " + a); System.out.println((a > b) || (a++ > c)); System.out.println("a = " + a); System.out.println((a == b) || (a++ > c)); System.out.println("a = " + a);}} 7.5 位运算符7.5.1 基本语法 位运算符的运算过程都是基于二进制的补码运算(1)左移:<< 运算规则:在一定范围内,数据每向左移动一位,相当于原数据*2。(正数、负数都适用) 【注意】当左移的位数n超过该数据类型的总位数时,相当于左移(n-总位数)位 3<<4 类似于 3*2的4次幂 => 3*16 => 48 -3<<4 类似于 -3*2的4次幂 => -3*16 => -48 (2)右移:>> 运算规则:在一定范围内,数据每向右移动一位,相当于原数据/2。(正数、负数都适用) 【注意】如果不能整除,向下取整。 69>>4 类似于 69/2的4次 = 69/16 =4 -69>>4 类似于 -69/2的4次 = -69/16 = -5 (3)无符号右移:>>> 运算规则:往右移动后,左边空出来的位直接补0。(正数、负数都适用) 69>>>4 类似于 69/2的4次 = 69/16 =4 -69>>>4 结果:268435451 (4)按位与:& 运算规则:对应位都是1才为1,否则为0。 1 & 1 结果为1 1 & 0 结果为0 0 & 1 结果为0 0 & 0 结果为0 9 & 7 = 1 -9 & 7 = 7 (5)按位或:| 运算规则:对应位只要有1即为1,否则为0。 1 | 1 结果为1 1 | 0 结果为1 0 | 1 结果为1 0 & 0 结果为0 9 | 7 //结果: 15 -9 | 7 //结果: -9 (6)按位异或:^ 运算规则:对应位一个为1一个为0,才为1,否则为0。 1 ^ 1 结果为0 1 ^ 0 结果为1 0 ^ 1 结果为1 0 ^ 0 结果为0 9 ^ 7 //结果为14 -9 ^ 7 //结果为-16 (7)按位取反:~ 运算规则:对应位为1,则结果为0;对应位为0,则结果为1。 ~0就是1 ~1就是0 ~9 //结果:-10 ~-9 //结果:8 7.6 条件运算符7.6.1 基本语法条件运算符格式:(条件表达式)? 表达式1:表达式21说明:条件表达式是boolean类型的结果,根据boolean的值选择表达式1或表达式2 如果运算后的结果赋给新的变量,要求表达式1和表达式2为同种或兼容的类型 public static void main(String[] args) { int i = (1==2 ? 100 : 200); System.out.println(i);//200 boolean marry = false;System.out.println(marry ? "已婚" : "未婚" ); double d1 = (m1 > m2)? 1 : 2.0;System.out.println(d1); int num = 12; System.out.println(num > 0? true : "num非正数");} 7.6.2 与if-else的转换关系凡是可以使用条件运算符的地方,都可以改写为if-else结构。反之,不成立。 开发中,如果既可以使用条件运算符,又可以使用if-else,推荐使用条件运算符。因为执行效率稍高。 //if-else实现获取两个数的较大值 int i1 = 10;int i2 = 20; int max;//声明变量max,用于记录i1和i2的较大值 if(i1 > i2){ max = i1;}else{ max = i2;} System.out.println(max); 7.7 运算符优先级运算符有不同的优先级,所谓优先级就是在表达式运算中的运算符顺序。 上一行中的运算符总是优先于下一行的。 优先级 运算符说明 Java运算符1 括号 ()、[]、{}2 正负号 +、-3 单元运算符 ++、--、~、!4 乘法、除法、求余 *、/、%5 加法、减法 +、-6 移位运算符 <<、>>、>>>7 关系运算符 <、<=、>=、>、instanceof8 等价运算符 ==、!=9 按位与 &10 按位异或 ^11 按位或 `12 条件与 &&13 条件或 `14 三元运算符 ? :15 赋值运算符 =、+=、-=、*=、/=、%=16 位赋值运算符 &=、`开发建议: 不要过多的依赖运算的优先级来控制表达式的执行顺序,这样可读性太差,尽量使用()来控制表达式的执行顺序。不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它分成几步来完成。例如: (num1 + num2) * 2 > num3 && num2 > num3 ? num3 : num1 + num2;————————————————原文链接:https://blog.csdn.net/fj123789/article/details/145671844
-
在信息安全领域,数据加密是保护数据机密性、完整性和可用性的重要手段之一。AES(Advanced Encryption Standard)作为当前广泛使用的对称加密算法,以其高效、安全的特点,在各类应用系统中扮演着重要角色。Java作为一门广泛应用于企业级开发的编程语言,其强大的加密库为开发者提供了丰富的加密解密工具。然而,直接使用Java的加密API进行AES加密解密时,可能会面临代码繁琐、理解难度高等问题。此时,Hutool这一Java工具类库便显得尤为实用。Hutool是一个小而全的Java工具类库,它简化了Java开发中常见的繁琐操作,包括但不限于日期处理、文件操作、加密解密等。在加密解密方面,Hutool提供了简洁易用的API,使得AES加密解密变得轻松简单。本文将详细介绍如何在Java项目中使用Hutool进行AES加密解密。一、Hutool简介与引入1.1 Hutool简介Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式编程的简洁性。Hutool中的工具方法来自于每个用户的精雕细琢,它涵盖了Java开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当。1.2 引入Hutool在Maven项目中,可以通过添加以下依赖来引入Hutool:<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>你的版本号</version> </dependency>请替换你的版本号为当前最新的Hutool版本号,以确保使用到最新的功能和修复。二、AES加密解密基础AES加密是一种对称加密算法,即加密和解密使用相同的密钥。AES支持三种长度的密钥:128位、192位和256位。在AES加密过程中,数据首先被分成多个固定长度的块(Block),然后每个块独立地进行加密。AES加密过程大致可以分为以下几个步骤:密钥扩展(Key Expansion):将用户提供的密钥扩展成一系列轮密钥(Round Keys)。初始轮(Initial Round):将明文块与初始轮密钥进行特定的操作(如异或、替换等)。中间轮(Intermediate Rounds):对初始轮的结果进行多次迭代加密,每次迭代使用不同的轮密钥。最终轮(Final Round):与中间轮类似,但可能包含一些额外的操作,如列混淆等。输出:最终轮的结果即为加密后的密文。解密过程则是加密过程的逆操作,使用相同的密钥和算法将密文还原为明文。三、使用Hutool进行AES加密解密3.1 加密在Hutool中,进行AES加密主要依赖于SecureUtil类和AES类。以下是一个简单的AES加密示例:import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.symmetric.AES; public class AesEncryptExample { public static void main(String[] args) { // 原始数据 String content = "Hello Hutool AES!"; // 密钥,AES要求密钥长度为128/192/256位 byte[] keyBytes = SecureUtil.generateKey(256).getEncoded(); // 创建AES加密对象,使用ECB/PKCS5Padding AES aes = SecureUtil.aes(keyBytes, "ECB/PKCS5Padding"); // 加密 byte[] encrypt = aes.encrypt(content); // 加密结果通常用于存储或传输,这里简单打印其Base64编码形式 String encryptHex = aes.encryptHex(content); System.out.println("加密结果(Hex): " + encryptHex); // 如果需要原始字节数组,则使用encrypt方法 // System.out.println(Base64.getEncoder().encodeToString(encrypt)); } }注意:在实际应用中,密钥keyBytes应安全地生成和存储,避免硬编码在代码中。3.2 解密解密过程与加密过程类似,只是将加密后的数据(密文)作为输入,通过AES解密对象还原为原始数据(明文)。// 假设encryptHex是之前加密得到的Hex字符串 String encryptHex = "..."; // 这里应该是加密后的Hex字符串 // 使用相同的密钥和算法进行解密 AES aes = SecureUtil.aes(keyBytes, "ECB/PKCS5Padding"); String decryptStr = aes.decryptStr(encryptHex); System.out.println("解密结果: " + decryptStr);四、AES加密模式与填充方式AES加密算法支持多种模式和填充方式,不同的模式和填充方式会影响加密解密的结果。在Hutool中,可以通过指定模式(如ECB、CBC等)和填充方式(如PKCS5Padding、NoPadding等)来创建AES加密解密对象。模式(Mode):决定了加密过程中密钥的使用方式。常见的模式有ECB、CBC、CFB、OFB等。填充方式(Padding):由于AES加密要求输入数据的长度必须是块大小的整数倍(AES块大小为128位),因此当输入数据长度不满足要求时,需要通过填充方式来达到要求。常见的填充方式有PKCS5Padding、PKCS7Padding、NoPadding等。在选择模式和填充方式时,需要根据具体的应用场景和安全需求来决定。例如,ECB模式虽然实现简单,但安全性较低,不适合用于需要高安全性的场景;而CBC模式则通过引入初始化向量(IV)来提高安全性,是较为常用的模式之一。五、安全性与性能考虑在使用AES加密解密时,安全性和性能是两个重要的考虑因素。安全性:确保密钥的安全生成、存储和传输是保障加密安全性的关键。此外,选择合适的加密模式和填充方式也是提高安全性的重要手段。性能:AES加密解密虽然高效,但在处理大量数据时仍可能对性能产生影响。因此,在性能敏感的应用中,需要合理设计加密解密策略,如采用异步处理、批量加密解密等方式来提高性能。六、总结Hutool作为一款实用的Java工具类库,为开发者提供了简洁易用的AES加密解密API。通过本文的介绍,读者可以了解到如何在Java项目中使用Hutool进行AES加密解密,包括加密解密的基本步骤、AES加密模式与填充方式的选择以及安全性和性能的考虑。希望本文能对读者在Java加密解密方面的学习和实践有所帮助。————————————————原文链接:https://blog.csdn.net/My_wife_QBL/article/details/141920322
-
1. 包装类在JAVA中,共有8种基本类型,分别是byte,short,long,int,double,float,char,boolean.但由于JAVA是一门纯面向对象的语言,而且8种基本并非继承于Object类,为了在泛型代码中可以⽀持基本类型,于是JAVA提供了包装类。 1.1 基本数据类型和对应的包装类 除了 Integer 和 Character, 其余基本类型的包装类都是⾸字⺟⼤写。 1.2 装箱和拆箱装箱:基本数据类型转换成包装类int i = 10;Integer ij = new Integer(i);//装箱 拆箱:包装类转换成基本数据类型int i = 10;Integer ij = new Integer(i);int j = ii.intValue();//拆箱 注意:现在都使用自动拆箱和自动装箱!!! 1.3 自动装箱和自动拆箱为了减少开发者的负担,java 提供了⾃动机制。 自动装箱 int i = 10; Integer j = i;//自动装箱12自动拆箱Integer j = 10;int a = j;//自动拆箱提问:下述代码分别输出什么,为什么? public class Test { public static void main(String[] args) { Integer a = 127; Integer b = 127; Integer c = 128; Integer d = 128; Integer e = -129; Integer f = -129; System.out.println(a == b);//true System.out.println(c == d);//false System.out.println(e == f);//false }}注意:a和b是应用类型,==比较的是身份,比较值要重写equals方法进行比较。 答:[-128,127]这个范围数字比较是会出现true,其他数字比较则会出现false。原因是Integer中常用的数字被放到了常量池里,常用数字的范围是[-128,127]. 2. 泛型我们以前学过的数组,只能存放指定类型的元素,但是因为所有类的父类都是Object类,所以数组类型是否可以创建成Object呢? class MyArray { private Object[] array = new Object[10]; public Object getPos(int pos) { return this.array[pos]; } public void setVal(int pos,Object val) { this.array[pos] = val; }}public class Test { public static void main(String[] args) { MyArray myArray = new MyArray(); myArray.setVal(0,10); myArray.setVal(1,"hello");//字符串也可以存放 String ret = myArray.getPos(1);//编译报错 System.out.println(ret); }}//1号下标本⾝就是字符串,但是确编译报错。必须进⾏强制类型转换 虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望 它只能够持有⼀种数据类型。⽽不是同时持有这么多类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象,让编译器去做检查。 此时,就需要把类型,作为参数传递。需要什么类型,就传⼊什么类型。 2.1 泛型的语法基础写法: class 泛型类名称<类型形参列表> {// 这⾥可以使⽤类型参数} 其他写法: class 泛型类名称<类型形参列表> extends 继承类/* 这⾥可以使⽤类型参数 */ {// 这⾥可以使⽤类型参数} 上述代码进⾏改写如下: class MyArray<T> { public Object[] array = new Object[10]; public T getPos(int pos) { return (T)this.array[pos]; } public void setVal(int pos,T val) { this.array[pos] = val; }}public class TestDemo { public static void main(String[] args) { MyArray<Integer> myArray = new MyArray<>();//1 myArray.setVal(0,10); myArray.setVal(1,12); int ret = myArray.getPos(1);//2 System.out.println(ret); myArray.setVal(2,"小朱小朱");//3 编译报错!!! }} 1. 注释1处,类型后加⼊ 指定当前类型2. 注释2处,不需要进行强制类型转换3. 注释3处,代码编译报错,此时因为在注释1处指定类当前的类型,此时编译器会在存放元素的时候帮助我们进行类型检查。 代码解释: 类名后的 < T >代表占位符,表⽰当前类是⼀个泛型类【规范】类型形参⼀般使⽤⼀个⼤写字⺟表示,常⽤的名称有:• E 表示Element• K 表示 Key• V 表示 Value• N 表示 Number• T 表示 Type• S, U, V 等等 - 第⼆、第三、第四个类型2.2 泛型类的使用泛型类<类型实参> 变量名; // 定义⼀个泛型类引⽤new 泛型类<类型实参>(构造⽅法实参); // 实例化⼀个泛型类对象 MyArray<Integer> list = new MyArray<Integer>(); 注意:泛型只能接受类,所有的基本数据类型必须使用包装类! 2.3 裸类型(Raw Type)裸类型是⼀个泛型类但没有带着类型实参,例如 MyArrayList 就是⼀个裸类型 MyArray list = new MyArray();2.4 擦除机制在编译时,Java 编译器会将泛型类型信息从代码中移除,这个过程就叫做类型擦除。擦除后,泛型类型会被替换为其边界类型(通常是 Object)或者指定的类型。同时也会在必要的地方插⼊类型转换以保持类型安全。 擦除前: class MyArray<T> { public Object[] array = new Object[10]; public T getPos(int pos) { return (T)this.array[pos]; } public void setVal(int pos,T val) { this.array[pos] = val; }} 擦除后: class MyArray { public Object[] array = new Object[10]; public Object getPos(int pos) { return this.array[pos]; } public void setVal(int pos, Object val) { this.array[pos] = val; }}2.5 泛型的上界在定义泛型类时,有时需要对传⼊的类型变量做⼀定的约束,可以通过类型边界来约束。 语法: class 泛型类名称<类型形参 extends 类型边界> {...} public class MyArray<E extends Number> {...}//只接受 Number 的⼦类型作为 E 的类型实参2.6 泛型方法语法: ⽅法限定符 <类型形参列表> 返回值类型 ⽅法名称(形参列表){...} 示例: public class Util {//静态的泛型⽅法 需要在static后⽤<>声明泛型类型参数public static <E> void swap(E[] array, int i, int j) {E t = array[i];array[i] = array[j];array[j] = t;}}2.7 通配符?用于在泛型的使⽤,即为通配符 请观察下述代码: class Message<T> { private T message ; public T getMessage() { return message; } public void setMessage(T message) { this.message = message; }}public class Test { public static void main(String[] args) { Message<String> message = new Message<>() ; message.setMessage("欢迎来到小朱的CSDN"); fun(message); } public static void fun(Message<String> temp){ System.out.println(temp.getMessage()); }} 以上程序会带来新的问题,如果现在泛型的类型设置的不是String,⽽是Integer. public class TestDemo { public static void main(String[] args) { Message<Integer> message = new Message() ; message.setMessage(99); fun(message); // 出现错误,只能接收String } public static void fun(Message<String> temp){ System.out.println(temp.getMessage()); }} 我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使⽤通配符"?"来处理 public class TestDemo { public static void main(String[] args) { Message<Integer> message = new Message() ; message.setMessage(55); fun(message); } // 此时使⽤通配符"?"描述的是它可以接收任意类型 public static void fun(Message<?> temp){ System.out.println(temp.getMessage()); }} 在"?"的基础上又产生了两个子通配符: 通配符上界: <? extends 上界><? extends Number>//可以传⼊的实参类型是Number或者Number的⼦类 通配符下界————————————————原文链接:https://blog.csdn.net/2401_82690001/article/details/145964769
-
一、IDEA 的下载IDEA(IntelliJ IDEA)是一款非常强大且深受开发者喜爱的 Java 集成开发环境(IDE),它提供了丰富的功能和便捷的开发体验,下面为大家详细介绍其下载步骤:访问官方网站:打开浏览器,输入 IntelliJ IDEA 的官方网址:https://www.jetbrains.com/idea/ 在官网首页,你可以看到关于 IDEA 的各种介绍和版本信息。选择版本:IDEA 分为社区版(Community Edition)和旗舰版(Ultimate Edition)。社区版是免费的,适合初学者和个人开发者,它提供了基本的 Java 开发功能;旗舰版则是付费版本,功能更为强大,支持更多的框架和技术,如 Web 开发、数据库管理等。根据自己的需求和预算,选择合适的版本。一般来说,对于学习 Java-servlet 开发,社区版就已经足够了。下载安装包:点击对应版本的“Download”按钮,根据你的操作系统(Windows、Mac 或 Linux)选择相应的安装包进行下载。等待下载完成:下载完成后,找到下载的安装包文件(通常是一个.exe 文件对于 Windows 系统,.dmg 文件对于 Mac 系统),双击运行安装程序,按照安装向导的提示逐步完成安装。二、Maven 的下载Maven 是一个项目管理和构建工具,它可以帮助我们管理项目的依赖关系、构建项目、生成文档等。使用 Maven 可以大大简化项目的管理和开发过程。以下是 Maven 的下载步骤:访问 Maven 官方网站:在浏览器中输入 Maven 的官方网址:https://maven.apache.org/ 。进入官网后,你可以了解到 Maven 的相关信息和文档。下载 Maven 安装包:在官网的首页,找到“Download”链接,点击进入下载页面。在下载页面中,你会看到不同版本的 Maven 安装包。建议选择最新的稳定版本进行下载。根据你的操作系统,选择对应的二进制压缩包(如.zip 文件对于 Windows 系统,.tar.gz 文件对于 Linux 和 Mac 系统)。 点击下载对应的版本我的gitee直接下载链接https://gitee.com/srte-7719/tool/tree/master/Maven三、Tomcat 的下载Tomcat 是 Apache 软件基金会开发的一个开源的 Web 应用服务器,它是运行 Java Web 应用程序的重要平台。下面是 Tomcat 的下载步骤:访问 Tomcat 官方网站:在浏览器中输入 Tomcat 的官方网址:https://tomcat.apache.org/ 。进入官网后,你可以看到关于 Tomcat 的介绍和下载链接。选择版本:在官网的首页,找到“Tomcat 版本”部分,选择你想要下载的 Tomcat 版本。目前较常用的版本有 Tomcat 8 和 Tomcat 9,建议选择最新的稳定版本。下载安装包:点击对应版本的“Download”按钮,进入下载页面。在下载页面中,你会看到不同类型的安装包。对于 Windows 系统,推荐下载“Windows Service Installer”(.msi 文件),它可以方便地将 Tomcat 安装为 Windows 服务;对于 Linux 和 Mac 系统,推荐下载“Core”部分的“tar.gz”格式的压缩包。安装(对于 Windows 系统的.msi 文件):下载完成后,双击运行.msi 文件,按照安装向导的提示进行安装。在安装过程中,你可以选择安装路径、配置端口号(默认是 8080)等参数。解压(对于 Linux 和 Mac 系统的.tar.gz 文件):将下载的.tar.gz 文件解压到你希望安装 Tomcat 的目录。例如,在 Linux 系统中,可以解压到“/usr/local/tomcat”目录。————————————————原文链接:https://blog.csdn.net/2402_83322742/article/details/145930212
-
1.synchronized修饰方法 synchronized public void increase(){ // synchronized修饰实例方法 count++; } public void increase1(){ synchronized (this){ // 用this作为锁对象 count++; } } synchronized public static void increase2(){ count++; } public void increase3(){ synchronized (Demo1.class){ // 通过反射拿到这个类对象 count++; } }一个.java文件编译 => .class(字节码文件) => 运行时.class文件被加载到JVM里面(就是说:JVM加载到内存中的数据结构就是类对象)解释:当JVM加载一个.class文件时,它会在内存中创建一个对应的数据结构,这个数据结构通常被称为“类对象”或“类结构”。这个类对象包含了类中定义的所有信息类对象包括:类的属性,名字,类型,权限,类方法,继承哪个类,实现了哪个接口…………2:synchronized是一个可重入锁那可重入锁是什么意思呢?一个线程针对一个对象连续加锁两次不会出现死锁(我们等会会细说死锁)。满足这个要求的就是可重入锁。我们拿一个代码进行举例: synchronized (locker){ synchronized (locker){ count++; } }按理说这个代码就卡住了:解释: 我先给第一次给locker加锁,按理说下面第二次这个加锁操作肯定阻塞等待第一次加锁释放锁,我这个synchrinized才能给locker加锁。但是第二次不加锁第一次的加的锁就没办法解锁。这完全就死了(这种情况就死锁了)但是!!!synchronized是可重入锁:就是可以连续给一个对象进行两次加锁对象头里会有一个计数器(这个线程给这个对象加锁一个,计数器就+1)解释对象头和计数器,线程给一个对象肯定会保存这个线程的信息对象在加锁时会保存这个线程的信息,这些信息保存在对象头(隐藏里的隐藏信息)的Mark Word中。每个对象都有一个对象头(Object Header),它包含了对象的一些元数据(譬如锁状态)。并且一个线程连续给一个对象加锁还会有一个计数器,这个计数器是也存在于对象头里面的Mark Word中。这一机制确保了同一个线程可以多次获得同一个对象的锁。解锁时:并且一个对象被加三把锁时加入计数器的值 = 3 ,被解锁的时候也不是真的解锁而是解一把锁计数器-1,直到减到0才是真正的解锁3:死锁3.1.死锁的引入刚才我们讲述了synchronized是一个可重入锁,我们给一个对象连续加锁并不会导致死锁(线程卡死了)。那什么情况下会出现死锁呢?接下来给大家看一个代码 public static void main(String[] args) { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() ->{ synchronized (locker1){ try { Thread.sleep(1000); //这个休眠1s真的很重要,开启t1线程立马就拿到了 locker1这把锁,如果不休眠的话 //很容易一下子这个线程就把两个锁都拿到了 } catch (InterruptedException e) { throw new RuntimeException(e); } // 嵌套的锁 synchronized (locker2){ System.out.println("t1加锁成功"); } } }); Thread t2 = new Thread(() -> { synchronized(locker2){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } // 嵌套的锁 synchronized(locker1){ System.out.println("t2 加锁成功"); } } }); t1.start(); t2.start(); }结果:什么都没有打印我们进入jconsole看一下两个线程的状态画个图解释一下所以说:所以两个线程都没有成功获得第二把锁 这个属于嵌套关系,线程A拿到locker1这把锁,又想要locker2这把锁(就可能出现死锁)但是如果是并列关系(线程A先释放前面的锁,再获取下一把锁)(就不会死锁)嵌套如何变成并列?改变代码结构! 这样就解决了,但是有时候代码结构不是那么好改变的。刚才说到死锁形成环。。。就会出现一个经典的问题——哲学家就餐问题3.2.哲学家就餐问题先搞一张图表示这个问题 这就是导致了死锁问题(怎么解决呢)说到怎么解决,就要知道形成死锁的几个必要条件,我们破坏其中一个就OK了有这四个条件(其中前两个条件是synchronized的属性(我们改变不了))1.互斥使用(锁的基本特性):一个线程拥有了锁A,另一个线程想要获取就只能阻塞等待2.不可抢占(锁的基本特性):和条件一差不多,另一个线程只能等那个线程释放锁A,不能抢占过来3.保持请求(代码结构):一个线程想要获得多把锁(嵌套,想要锁B但是又不想释放自己的锁B)(其实并形成环,你想要几个锁都没问题)4.循环等待(代码结构):(条件三导致的条件四),等待的关系形成环了条件三:其实有时候的需求就是需要进行获取多把锁,这个结构不好改变条件四:我们约定好了加锁的顺序,就可以避免循环等待(针对锁进行编号,先加小锁再加大锁)所以说哲学家就餐问题就可以这么解决!!!我们可以规定每个哲学家只能先拿起数字小的筷子,哲学家B先拿起筷子,然后最后哲学家A面前只剩下了一个筷子5,但是他现在不能拿了(这个数字比较大)所以哲学家E就能够吃面条了,然后就通了。 上述就是synchronized—死锁问题的全部内容了,死锁的出现,会让我们的程序陷入一个死循环的问题,但是我们只要知道死锁的成因,至少就知道了如何解决这个问题啦~~~预知后事如何,请听下回分解~~~能看到这里相信您一定对小编的文章有了一定的认可。有什么问题欢迎各位大佬指出欢迎各位大佬评论区留言修正~~————————————————原文链接:https://blog.csdn.net/2302_80639556/article/details/145079283
-
概述在 Spring 框架中,BeanPostProcessor 和 BeanFactoryPostProcessor 是两个重要的接口,用于在 Spring 容器管理 Bean 的生命周期过程中提供自定义的扩展点。它们允许开发者在 Spring 容器实例化和初始化 bean 的过程中进行额外的处理。 两者的定义和作用BeanPostProcessor:Spring 的 BeanPostProcessor 名称后置处理器,这是一个拓展机制,用于在 Spring 容器实例化、配置完成之后,在初始化前和初始化后,对 Bean 进行再加工的自定义处理。BeanFactoryPostProcessor:Spring 的 BeanFactoryPostProcessor 接口定义了一个方法,允许在 Spring 容器标准初始化之后、实例化任何 Bean 之前,对容器的内部 BeanFactory 进行修改。两者的区别与联系先说区别执行时机不同:BeanPostProcessor 在每个 Bean 实例化和初始化前后的过程中执行;BeanFactoryPostProcessor 在 Spring 容器的启动过程中执行,其执行时机是在容器标准初始化之后,所有的 BeanDefinition 已经加载到容器中,但还没有实例化任何 Bean 之前。作用对象不同:BeanPostProcessor 作用于每一个 Bean 实例,允许对 Bean 实例进行修改,例如修改属性、生成代理对象等。BeanFactoryPostProcessor 作用于 BeanFactory 本身,允许修改 Bean 定义,影响容器中的 Bean 配置,例如修改属性值、动态注册 Bean 定义等。再说联系Spring 容器的扩展机制:这两个接口都属于 Spring 容器的扩展机制,用于增强容器的功能和灵活性,都提供了在 Spring 容器初始化过程中进行自定义处理的扩展点,允许开发者在框架执行核心逻辑之前或之后插入自己的逻辑。需要在 Spring 配置中进行注册:无论是通过 XML 配置文件还是通过注解方式,这两个接口的实现类都需要在 Spring 配置中进行注册,以便 Spring 容器能够识别和调用它们。使用场景BeanPostProcessor动态代理:在 Bean 初始化后创建代理对象。属性检查或修改:在 Bean 初始化前检查或修改属性。BeanFactoryPostProcessor属性占位符配置:修改 Bean 定义中的占位符值。条件性 Bean 注册:根据条件动态注册或修改 Bean 定义。BeanPostProcessor 详解接口定义Spring 容器会为每个 Bean 创建实例后,分别调用 BeanPostProcessor 接口的两个方法: public interface BeanPostProcessor { @Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; }} 方法详解postProcessBeforeInitialization在 Bean 初始化之前调用,开发者可以通过该方法对 Bean 进行自定义处理,包括修改 Bean 的属性值、添加一些特殊处理等。此方法的第一个参数即本次创建的 Bean 对象,第二个参数即本次创建 Bean 的名字。需要注意处理完成后需要将 Bean 作为返回值返回,归还给 Spring 管理。 postProcessAfterInitialization在 Bean 初始化之后调用,开发者可以通过该方法对 Bean 进行自定义处理,比如动态代理、AOP 增强等。此方法的第一个参数即本次创建的 Bean 对象,第二个参数即本次创建 Bean 的名字。需要注意处理完成后需要将 Bean 作为返回值返回,归还给 Spring 管理。 注意:在实战中很少对 Spring 中的对象进行初始化的操作,所以区分 Before 和 After 的意义不是很大,我们只需要实现一种即可,推荐实现 After 方法,另外一个 Before 方法就按默认即可。 注意:BeanPostProcessor 后置处理器会对 Spring 创建的所有对象进行处理。 示例代码在某些情况下,我们可能需要在应用程序启动时扫描特定的注解,并在 Bean 初始化时进行处理。例如,我们希望在 Spring Boot应用中扫描所有带有特定注解的类,并在它们被初始化时打印日志和执行方法。 @Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface XUE {} @XUE@Componentpublic class XueWei { public void hello() { System.out.println("Hello XW!"); }} @Componentpublic class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Class<?> targetClass = bean.getClass(); if (targetClass.isAnnotationPresent(XUE.class)) { // 获取到了自定义注解标记的类 System.out.println("扫描到了标记 @XUE 注解的类:" + targetClass.getName()); Method[] methods = bean.getClass().getDeclaredMethods(); for (Method method : methods) { // 执行指定方法 if (!"hello".equals(method.getName())) { continue; } try { // 通过反射执行该类的所有的方法 method.invoke(bean); } catch (Exception e) { System.out.println("执行报错:" + e); } } } return bean; }} 基于 XML 的方式注解: <!-- 注册后置处理器 --><bean id="processor" class="world.xuewei.processor.MyProcessor"/> 优点与缺点先说优点灵活性:BeanPostProcessor 提供了一种灵活的机制,允许开发人员在 Spring 容器实例化、配置和初始化 Bean 的过程中介入,可以对 Bean 进行自定义的操作和处理。可扩展性:开发人员可以根据实际需求编写自定义的 BeanPostProcessor 实现,实现各种功能,如自动装配、依赖注入、AOP、事务管理等。AOP 实现基础:Spring AOP 的实现正是基于 BeanPostProcessor 机制。通过 BeanPostProcessor,Spring 可以在 Bean 初始化的过程中动态地生成代理对象,实现 AOP 的横切逻辑。再说缺点性能影响:使用 BeanPostProcessor 可能会对应用程序的性能产生一定的影响,特别是在大规模应用中,由于需要对所有的 Bean 进行处理,可能会增加应用程序的启动时间和内存消耗。复杂性:如果不正确地使用 BeanPostProcessor,可能会导致应用程序的复杂性增加,增加代码的维护成本和理解难度。潜在的问题:由于 BeanPostProcessor 可以对所有的 Bean 进行操作,因此可能会引入一些潜在的问题,如循环依赖、死锁等。注意事项避免循环依赖:使用 BeanPostProcessor 时,要注意避免循环依赖的问题,避免出现 Bean 依赖循环导致的应用程序启动失败。谨慎处理 Bean 初始化逻辑:在编写 BeanPostProcessor 实现时,要谨慎处理 Bean 初始化逻辑,确保不会影响到 Bean 的正常初始化过程。注意性能影响:如果应用程序启动时间较长或者内存消耗较高,可以考虑减少 BeanPostProcessor 的数量,或者优化 BeanPostProcessor 的实现,以减少性能影响。优先级设置:可以通过实现 Ordered 接口或者在 @Order 注解中设置优先级来控制多个 BeanPostProcessor 的执行顺序,确保处理顺序的正确性。测试与调试:在使用 BeanPostProcessor 时,建议进行充分的测试和调试,确保 BeanPostProcessor 的实现逻辑正确,不会引入潜在的问题。BeanFactoryPostProcessor 详解接口定义通过实现 BeanFactoryPostProcessor,可以在 Bean 实例化之前对 Bean 的定义进行修改,适用于对 Bean 配置的元数据进行全局修改,例如修改属性值,替换属性占位符等。 public interface BeanFactoryPostProcessor { void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;} 方法详解postProcessBeanFactory在容器实例化任何 Bean 之前执行,可以修改应用程序上下文的内部 Bean 定义,调整 Bean 的配置元数据。 适用场景@Componentpublic class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // 场景一:修改已存在的 BeanDefinition try { // 获取指定 Bean 的定义 BeanDefinition beanDefinition = beanFactory.getBeanDefinition("student"); // 修改 BeanDefinition 属性信息 MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); propertyValues.addPropertyValue("name", "薛伟"); // 修改作用域 beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); // 快速初始化 Bean Object student = beanFactory.getBean("student"); System.out.println(student); // ... } catch (Exception e) { System.err.println("修改 Student 的 BeanDefinition 属性失败!" + e); } // 场景二:动态注册 BeanDefinition(Spring 与 MyBatis 整合就是使用的这种) DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory; GenericBeanDefinition definition = new GenericBeanDefinition(); definition.setBeanClass(Teacher.class); definition.getPropertyValues().addPropertyValue("name", "XUEW"); factory.registerBeanDefinition("teacher", definition); // 场景三:加载外部配置文件 try { Properties props = PropertiesLoaderUtils.loadAllProperties("db.properties"); for (String key : props.stringPropertyNames()) { System.setProperty(key, props.getProperty(key)); } } catch (IOException e) { throw new RuntimeException("Failed to load properties file", e); } // 场景四:条件性修改 Bean 定义 String os = System.getProperty("os.name"); if ("Mac OS X".equals(os)) { BeanDefinition osInfo = beanFactory.getBeanDefinition("osInfo"); MutablePropertyValues infoPropertyValues = osInfo.getPropertyValues(); infoPropertyValues.addPropertyValue("name", os); infoPropertyValues.addPropertyValue("desc", "苹果系统"); } // 场景五:快速初始化 Bean System.out.println(beanFactory.getBean("student")); System.out.println(beanFactory.getBean("teacher")); System.out.println(beanFactory.getBean("osInfo")); // 设置当前环境 System.setProperty("environment.active", "prod"); }} 示例场景假设我们有一个应用程序需要连接不同环境下的 Redis 实例,根据运行环境(如开发、测试、生产)动态修改 Redis 的连接信息。 创建一个名为 redis.properties 的配置文件,包含不同环境下的 Redis 连接信息。test.redis.host=test.xuewei.worldtest.redis.port=6379test.redis.password=123456prod.redis.host=prod-redis-serverprod.redis.port=16379prod.redis.password=P@ssW0RD 创建一个 Bean 类 RedisConfig,包含 Redis 连接属性。@Data@NoArgsConstructor@AllArgsConstructor@Builderpublic class RedisConfig { private String host; private int port; private String password; } 实现一个自定义的 BeanFactoryPostProcessor,根据运行时的环境修改 Bean 定义。@Componentpublic class RedisEnvironmentBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware { private Environment environment; @Override public void setEnvironment(Environment environment) { this.environment = environment; } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { String env = System.getProperty("environment.active"); BeanDefinition beanDefinition = beanFactory.getBeanDefinition("redisConfig"); MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); propertyValues.add("host", environment.getProperty(env + ".redis.host")); propertyValues.add("port", environment.getProperty(env + ".redis.port", Integer.class)); propertyValues.add("password", environment.getProperty(env + ".redis.password")); System.out.println(beanFactory.getBean("redisConfig")); }} 使用 @Configuration 和 @PropertySource 注解配置 Spring 上下文,并启用属性占位符解析。@Configuration@ComponentScan(basePackages = {"world.xuewei.dao", "world.xuewei.service", "world.xuewei.processor"})@EnableAspectJAutoProxy(proxyTargetClass = true)@PropertySource({"classpath:/student.properties", "classpath:/redis.properties"})public class AppConfig { @Bean public RedisConfig redisConfig() { return new RedisConfig(); }} 优点与缺点先说优点修改 Bean 定义:BeanFactoryPostProcessor 可以在 Bean 实例化之前修改 Bean 的定义属性,这提供了一种灵活的方式来调整 Bean 的配置。动态配置:它可以用于根据运行环境动态配置 Bean 属性,如使用外部配置文件的属性占位符进行配置。这有助于实现应用程序的配置灵活性和可维护性。增强功能:可以用来增加自定义逻辑,例如自动注册额外的 Bean 定义,动态修改现有的 Bean 配置,或者实现其他自定义处理逻辑。属性占位符解析:通过实现特定的 BeanFactoryPostProcessor(如 PropertyPlaceholderConfigurer),可以将外部配置文件中的属性值注入到 Spring Bean 中,这使得应用程序的配置管理更加灵活和集中化。再说缺点复杂性增加:使用 BeanFactoryPostProcessor 可能增加应用程序配置的复杂性,尤其是在处理大量的自定义逻辑时,可能会使配置难以理解和维护。调试困难:由于 BeanFactoryPostProcessor 在 Spring 容器初始化的早期阶段执行,调试可能会变得复杂。问题可能在 Bean 实例化之前发生,因此追踪和解决这些问题可能比较困难。依赖顺序问题:使用多个 BeanFactoryPostProcessor 时,需要注意它们的执行顺序,否则可能会导致不可预期的行为。正确管理多个 BeanFactoryPostProcessor 的执行顺序可能会增加开发复杂性。性能影响:由于 BeanFactoryPostProcessor 在 Spring 容器启动时执行,包含复杂逻辑的处理器可能会增加应用程序启动时间。如果处理逻辑比较重,会影响应用程序的启动性能。注意事项确保执行顺序:多个 BeanFactoryPostProcessor 实例可能会存在相互依赖关系。因此,了解并控制它们的执行顺序非常重要。可以实现 Ordered 接口或 PriorityOrdered 接口来指定执行顺序。官方文档中明确指出 BeanFactoryPostProcessor 不能使用 @Order 注解。 避免复杂逻辑:由于 BeanFactoryPostProcessor 在 Spring 容器启动期间执行,避免在其实现中引入复杂或耗时的逻辑。复杂逻辑可能会延长应用程序启动时间,影响性能。 确保线程安全:BeanFactoryPostProcessor 可能会在多线程环境中被调用,因此确保实现的线程安全非常重要。避免使用非线程安全的数据结构或方法。 处理依赖关系:避免在 BeanFactoryPostProcessor 中直接引用其他 Bean,因为这些 Bean 可能还没有被实例化。如果必须引用其他 Bean,可以通过 BeanFactory 进行延迟获取。 @Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { MyBean myBean = beanFactory.getBean(MyBean.class); // Use myBean} 避免循环依赖:由于 BeanFactoryPostProcessor 可能会修改 Bean 定义,从而影响其他 BeanFactoryPostProcessor 的行为,确保这些修改不会引起循环依赖。 使用正确的应用场景:BeanFactoryPostProcessor 适用于需要在 Bean 实例化之前修改 Bean 定义的场景。对于在 Bean 实例化之后需要执行的逻辑,考虑使用 BeanPostProcessor。 小心静态资源的使用:避免在 BeanFactoryPostProcessor 中使用静态资源或全局变量,除非这些资源是线程安全的并且适合在 Spring 应用程序启动期间使用。 记录日志和异常处理:在 postProcessBeanFactory 方法中添加适当的日志记录和异常处理,以便在启动时发生问题时能快速定位和解决问题。 @Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { try { // Your processing logic } catch (Exception e) { // Log and handle the exception throw new BeansException("Error processing bean factory", e); }} 配置管理:如果 BeanFactoryPostProcessor 依赖外部配置,确保配置文件加载顺序正确,并且配置文件中的值能够正确解析。 避免对 Bean 的直接操作:尽量避免在 BeanFactoryPostProcessor 中直接操作 Bean 实例。更多的操作应当在 Bean 定义层面进行,而非实例层面。———————————————— 原文链接:https://blog.csdn.net/weixin_53287520/article/details/139484810
-
1. 背景在开发大型Java应用程序时,内存管理是一个至关重要的环节。Java虚拟机(JVM)中的垃圾回收机制(GC)在一定程度上可以帮助程序员管理内存,但随着程序规模的扩大和复杂度的提升,内存使用和GC的效率成为了性能瓶颈。本篇文章将介绍如何通过调优GC策略、减少对象分配以及排查内存泄漏来优化Java应用的内存使用,提升程序的稳定性和运行效率。2. JVM内存模型JVM将内存分为堆内存(Heap)和栈内存(Stack)。堆内存是用于存储所有Java对象的,而栈内存则存储方法调用和局部变量。2.1 JVM内存分区 • Eden区:新创建的对象首先存放于此区。 • Survivor区:经过GC后未被清除的对象会转移到Survivor区。 • 老年代(Old Generation):在Survivor区存活时间较长的对象会进入老年代。 • 永久代/元数据区(Permanent/Metaspace):用于存储类的元数据。3. 垃圾回收机制(GC)调优3.1 GC的种类在JVM中,有几种不同的垃圾回收器,适用于不同场景: • Serial GC:单线程GC,适用于小型应用。 • Parallel GC:多线程GC,适用于吞吐量优先的应用。 • CMS(Concurrent Mark-Sweep)GC:适用于低延迟场景,GC过程与应用线程并行运行。 • G1 GC:设计用于处理大内存堆,兼顾吞吐量和低延迟的需求。3.2 如何选择合适的GC策略通过分析应用的性能需求,可以选择合适的GC策略。例如,如果应用要求较低的延迟,可以考虑使用CMS或G1 GC。在服务器上运行的高并发应用,则可能更适合使用Parallel GC。3.3 调优GC参数通过调整JVM的启动参数,可以进一步优化GC的行为:-Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200这些参数可以帮助我们设置堆内存的初始大小、最大大小以及垃圾回收器类型等。通过合理的GC调优,可以减少应用程序的停顿时间和内存抖动。4. 内存泄漏问题的排查4.1 常见的内存泄漏原因 • 静态变量持有对象引用:静态变量的生命周期与JVM相同,如果静态变量没有被正确释放,它们引用的对象将不会被GC回收。 • 未关闭的资源(如文件流、数据库连接等):如果使用完毕后未及时关闭,可能会导致内存泄漏。 • 不正确的数据结构使用:例如,使用HashMap等数据结构时,未正确清除过期或不再使用的对象。4.2 使用工具分析内存泄漏可以使用以下工具来排查内存泄漏: • VisualVM:JDK自带的内存分析工具,可以帮助监控堆内存使用情况。 • Eclipse Memory Analyzer (MAT):强大的内存分析工具,可以帮助分析内存快照,找出内存泄漏的根源。通过在应用运行过程中生成堆快照(heap dump),可以使用这些工具分析内存中哪些对象占用了大量空间,以及它们的引用路径,从而帮助我们排查出具体的内存泄漏问题。5. 优化内存使用的最佳实践5.1 避免频繁的对象创建频繁创建短生命周期的小对象会增加GC压力,影响性能。可以考虑使用对象池(Object Pool)或StringBuilder等技术来避免频繁创建不必要的对象。5.2 使用弱引用(WeakReference)对于一些无需强引用的对象,可以使用弱引用(WeakReference)或软引用(SoftReference)来避免对象长时间驻留在内存中,从而减少内存压力。5.3 注意集合类的使用在使用集合类(如ArrayList, HashMap等)时,应该根据实际需要指定合适的初始大小,避免动态扩容带来的额外开销。6. 结论Java内存管理虽然有JVM的垃圾回收机制来辅助,但对于大型复杂应用,合理的GC调优、对象分配优化和内存泄漏排查仍是不可忽视的工作。通过以上方法,可以有效减少内存使用,优化Java应用的性能和稳定性。————————————————原文链接:https://blog.csdn.net/weixin_42063627/article/details/142310062
-
在 Java 开发的广阔天地里,开发效率是每个开发者都关注的重点。DeepSeek 作为人工智能技术在代码辅助领域的佼佼者,凭借强大的代码生成、智能补全以及深度代码理解能力,为开发者提供了极大的便利。而 Eclipse,作为一款经典且被广泛使用的 Java 集成开发环境,拥有丰富的插件生态和完善的开发工具。当我们将 DeepSeek 接入 Eclipse,就如同为 Eclipse 插上了智能的翅膀,能够显著提升 Java 开发的效率与质量。本文将详细介绍在 Eclipse 中接入 DeepSeek 的方法,帮助开发者充分利用这两者的结合,在 Java 开发中如虎添翼。二、DeepSeek 功能概述(一)强大的代码生成能力DeepSeek 能够根据自然语言描述,精准生成高质量的 Java 代码片段。无论是简单的方法编写,比如 “编写一个 Java 方法,用于计算两个整数的乘积”,还是复杂的类和功能模块创建,例如 “创建一个 Java 类,实现文件的读写操作,并包含异常处理机制”,它都能快速给出合理且可运行的代码示例。DeepSeek 生成的代码不仅实现了基本功能,还遵循 Java 的编码规范,添加了必要的注释,让代码更易于理解和维护,大大节省了开发者从头编写代码的时间和精力。(二)深度代码理解与分析对于庞大复杂的 Java 项目代码库,DeepSeek 具备深度解析的能力。在接手一个全新的大型 Java 项目时,开发者往往需要花费大量时间去梳理代码逻辑、理解各个类和方法之间的关系。DeepSeek 可以帮助开发者快速理清项目结构,识别关键代码路径,理解代码的核心功能和业务逻辑,从而显著缩短熟悉项目的周期,加快开发进度。(三)智能代码补全在编写 Java 代码过程中,DeepSeek 的智能补全功能能够实时预测开发者接下来可能输入的代码。它会根据当前代码的上下文,包括类的定义、方法的参数、已导入的包等信息,以及 Java 编程的常见模式,提供准确且有针对性的补全建议。例如,当在一个操作数据库的类中编写代码时,它会优先提供与数据库连接、查询、更新等相关的方法和类的补全选项,让代码编写更加流畅高效,减少因思考代码细节而产生的停顿。三、在 Eclipse 中接入 DeepSeek 的前期准备(一)安装 Eclipse确保你已经安装了合适版本的 Eclipse。如果尚未安装,可以前往 Eclipse 官方网站(Eclipse Downloads | The Eclipse Foundation ),根据你的操作系统和开发需求,选择相应的 Eclipse 安装包进行下载。安装过程相对简单,按照安装向导的提示逐步操作即可完成。安装完成后,启动 Eclipse。(二)获取 DeepSeek API 密钥要在 Eclipse 中使用 DeepSeek,首先需要获取其 API 密钥。访问 DeepSeek 的官方平台,进行注册并登录账号。在个人设置或 API 管理相关页面,找到生成 API 密钥的选项,按照平台提供的详细指引,生成属于你自己的唯一 API 密钥。请务必妥善保管这个密钥,因为它是连接你与 DeepSeek 服务的关键凭证,一旦泄露,可能会导致安全风险和服务滥用。四、具体接入步骤(一)安装 DeepSeek 插件打开 Eclipse,点击菜单栏中的 “Help”(帮助),选择 “Eclipse Marketplace...”(Eclipse 应用市场)。在弹出的 Eclipse 应用市场窗口中,找到搜索框。在搜索框中输入 “DeepSeek”,然后点击搜索按钮。在搜索结果中找到对应的 DeepSeek 插件。点击插件右侧的 “Install”(安装)按钮,Eclipse 会开始下载并安装插件。在安装过程中,可能会出现一些提示询问你是否信任插件的来源等信息,按照提示进行确认操作。安装完成后,点击 “Finish”(完成),然后根据提示重启 Eclipse,使插件生效。(二)配置 API 密钥重启 Eclipse 后,点击菜单栏中的 “Window”(窗口),选择 “Preferences”(首选项)。在弹出的首选项窗口中,找到 “DeepSeek” 选项(通常在列表的较下方,如果找不到,可以在搜索框中输入 “DeepSeek” 进行搜索)。点击 “DeepSeek” 选项,在右侧的设置页面中,将之前获取的 DeepSeek API 密钥粘贴到 “API Key” 对应的输入框中。点击 “Test Connection”(测试连接)按钮,检查 Eclipse 是否能够成功连接到 DeepSeek 服务。如果连接成功,会弹出提示框显示连接正常;若连接失败,请仔细检查 API 密钥是否正确,以及网络连接是否稳定。确认连接成功后,点击 “OK” 保存设置。此时,Eclipse 与 DeepSeek 之间的连接已初步建立。(三)使用 DeepSeek 功能打开一个 Java 项目或新建一个 Java 文件。将光标定位到需要使用 DeepSeek 功能的代码位置,比如在类中需要编写一个新的方法,或者在方法体中需要补充代码逻辑。可以通过快捷键(默认快捷键可在插件设置中查看和修改)或者右键点击鼠标,在弹出的菜单中选择与 DeepSeek 相关的操作选项。常见的操作选项包括 “Generate Code with DeepSeek”(使用 DeepSeek 生成代码)、“Code Completion by DeepSeek”(DeepSeek 智能补全)等。以 “Generate Code with DeepSeek” 为例,选择该选项后,会弹出一个输入框,在其中输入自然语言描述,例如 “编写一个 Java 方法,实现对 List 集合中的元素进行排序”。输入完成后,点击 “OK”,等待 DeepSeek 处理请求。DeepSeek 会根据你的描述生成相应的 Java 代码,并将其插入到光标所在的位置。你可以对生成的代码进行审查、修改和完善,使其符合项目的具体需求和编码规范。五、使用技巧与注意事项(一)使用技巧精准描述需求:在使用 DeepSeek 生成代码时,尽可能提供详细、准确的自然语言描述。描述越清晰,生成的代码就越符合你的预期。例如,不要简单地说 “写个排序方法”,而是详细说明 “用 Java 编写一个使用快速排序算法对整数数组进行排序的方法,并返回排序后的数组”,这样可以让 DeepSeek 生成更具针对性和实用性的代码。利用上下文信息优化补全:在使用智能补全功能时,充分利用当前代码的上下文环境。DeepSeek 会根据上下文信息,如当前类的继承关系、已定义的变量和方法、导入的包等,提供更精准的补全建议。比如在一个继承自 HttpServlet 的类中编写代码时,它会优先提供与 HTTP 请求处理相关的方法和类的补全选项。多轮交互优化代码质量:如果 DeepSeek 生成的代码不完全符合需求,可以通过多轮交互来优化。例如,生成的代码实现了基本功能,但性能不佳,你可以在生成的代码基础上,再次使用 DeepSeek 功能,输入 “优化这段代码的性能,减少时间复杂度”,DeepSeek 会根据已有代码和新的指令,进一步调整代码,提高代码的质量和效率。(二)注意事项保护 API 密钥安全:API 密钥是连接你与 DeepSeek 服务的重要凭证,一旦泄露,可能会带来严重的安全问题。不要将 API 密钥分享给他人,避免在不安全的网络环境或不可信的设备上使用。如果发现 API 密钥有泄露风险,应立即在 DeepSeek 官方平台上重新生成新的密钥,并在 Eclipse 中及时更新配置。遵守服务使用规则:DeepSeek 的服务可能存在一些使用限制,如调用频率限制、使用时长限制、请求内容限制等。在使用过程中,要仔细阅读并严格遵守 DeepSeek 的服务条款和使用规则。避免因频繁调用服务或超出使用配额而导致服务异常或被限制使用。如果遇到调用失败的情况,首先检查是否违反了服务使用规则,并根据提示进行相应调整。严格审查生成的代码:虽然 DeepSeek 生成的代码质量较高,但由于其基于算法和大量数据生成,可能存在一些潜在问题,如与项目特定的编码风格不一致、在某些特殊场景下存在逻辑错误等。在将生成的代码应用到实际项目中之前,务必进行严格的代码审查。可以从代码的正确性、可读性、可维护性以及安全性等多个角度进行审查,确保代码符合项目的要求和标准。六、总结通过在 Eclipse 中接入 DeepSeek,Java 开发者能够充分利用人工智能技术的优势,显著提升开发效率和代码质量。从前期的安装准备,到具体的接入步骤,再到使用过程中的技巧和注意事项,每一个环节都为打造高效智能的开发环境提供了有力支持。希望本文能够帮助你顺利地将 DeepSeek 融入到 Eclipse 的开发流程中,让你在 Java 开发的道路上更加得心应手,创造出更优秀的 Java 应用程序。随着人工智能技术的不断发展,相信会有更多类似的强大工具出现,为 Java 开发领域带来更多的创新和变革,让我们共同期待并积极拥抱这些变化。————————————————原文链接:https://blog.csdn.net/fq1986614/article/details/145885315
-
1. 引言JSON 是现代软件开发中常用的数据交换格式,尤其在微服务和前后端分离的架构中更是必不可少。本文将对 Java 中四大主流 JSON 解析库——Hutool、Fastjson2、Gson 和 Jackson 进行性能测试和对比分析,通过实测 20 万条数据解析,揭示各库在批量和逐条处理中的表现。测试结果仅供参考!!! 请多次测试再抉择2. 环境与依赖2.1 环境信息操作系统:Window11JDK 版本:jdk1.8.0_281CPU : AMD Ryzen 9 7945HX内存:32GB2.2 Maven依赖在项目的 pom.xml 文件中引入以下依赖: <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.10.1</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.52</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.14.2</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.35</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>3. 测试代码3.1 数据模型定义一个简单的实体对象:import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor; /** * @author 阿水 */@Data@AllArgsConstructor@NoArgsConstructorpublic class Variable { private int id; private String name; private double value; private String description; private String type;}3.2 测试数据生成模拟 20 万条数据用于测试: // 生成测试数据 public static List<Variable> generateData(int size) { List<Variable> list = new ArrayList<>(size); for (int i = 0; i < size; i++) { Variable data = new Variable(); data.setId(i); data.setName("Name" + i); data.setValue(Math.random() * 1000); data.setDescription(IdUtil.fastSimpleUUID()); data.setType(IdUtil.fastSimpleUUID()+i); list.add(data); } return list; }3.3 四大库序列化与反序列化测试import cn.hutool.core.util.IdUtil;import cn.hutool.json.JSONUtil;import com.alibaba.fastjson2.JSON;import com.fasterxml.jackson.core.type.TypeReference;import com.fasterxml.jackson.databind.ObjectMapper;import com.google.gson.Gson;import com.google.gson.reflect.TypeToken;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;import java.util.List;import java.util.stream.Collectors; /** * @description: JSON 序列化、解析性能测试 * @author 阿水 */@Slf4jpublic class JsonBenchmarkTest { //数据总条数 public static int dataSize = 200000; //测试次数 public static int iterations = 10; public static void main(String[] args) throws Exception { // 生成测试数据 List<Variable> testData = generateData(dataSize); log.info("测试数据总条数为: {} 条", dataSize); log.info("以下测试结果均为进行 {} 次计算之后,耗费时间取平均值计算得出。", iterations); // 序列化测试 String jsonString = serializationTest(testData); log.info("JSON 数据总大小为: {} 字节", jsonString.length()); // 批量解析测试 log.info("===== 使用批量解析 JSON(即解析集合API)====="); batchParseTest(jsonString); // 单条解析测试 log.info("===== 循环遍历逐个解析 JSON API ====="); singleParseTest(jsonString); // 单条解析并插入集合测试 log.info("===== 循环遍历逐个解析 JSON并插入集合 API====="); singleParseAndAddListTest(jsonString); } // 生成测试数据 public static List<Variable> generateData(int size) { List<Variable> list = new ArrayList<>(size); for (int i = 0; i < size; i++) { Variable data = new Variable(); data.setId(i); data.setName("Name" + i); data.setValue(Math.random() * 1000); data.setDescription(IdUtil.fastSimpleUUID()); data.setType(IdUtil.fastSimpleUUID()+i); list.add(data); } return list; } /** * 序列化测试 */ private static String serializationTest(List<Variable> testData) throws Exception { String jsonResult = null; // Hutool long hutoolTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); String hutoolJson = JSONUtil.toJsonStr(testData); long end = System.currentTimeMillis(); hutoolTotal += (end - start); if (i == 0) jsonResult = hutoolJson; // 保存结果 } log.info("HuTool 序列化平均耗时: {} ms", hutoolTotal / iterations); // Fastjson2 long fastjsonTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); String fastjsonJson = JSON.toJSONString(testData); long end = System.currentTimeMillis(); fastjsonTotal += (end - start); } log.info("Fastjson2 序列化平均耗时: {} ms", fastjsonTotal / iterations); // Gson Gson gson = new Gson(); long gsonTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); String gsonJson = gson.toJson(testData); long end = System.currentTimeMillis(); gsonTotal += (end - start); } log.info("Gson 序列化平均耗时: {} ms", gsonTotal / iterations); // Jackson ObjectMapper objectMapper = new ObjectMapper(); long jacksonTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); String jacksonJson = objectMapper.writeValueAsString(testData); long end = System.currentTimeMillis(); jacksonTotal += (end - start); } log.info("Jackson 序列化平均耗时: {} ms", jacksonTotal / iterations); return jsonResult; } /** * 批量解析测试 */ private static void batchParseTest(String jsonString) throws Exception { // Hutool long hutoolTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); List<Variable> result = JSONUtil.toList(JSONUtil.parseArray(jsonString), Variable.class); long end = System.currentTimeMillis(); hutoolTotal += (end - start); } log.info("HuTool 批量解析平均耗时: {} ms", hutoolTotal / iterations); // Fastjson2 long fastjsonTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); List<Variable> result = JSON.parseArray(jsonString, Variable.class); long end = System.currentTimeMillis(); fastjsonTotal += (end - start); } log.info("Fastjson2 批量解析平均耗时: {} ms", fastjsonTotal / iterations); // Gson Gson gson = new Gson(); long gsonTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); List<Variable> result = gson.fromJson(jsonString, new TypeToken<List<Variable>>() {}.getType()); long end = System.currentTimeMillis(); gsonTotal += (end - start); } log.info("Gson 批量解析平均耗时: {} ms", gsonTotal / iterations); // Jackson ObjectMapper objectMapper = new ObjectMapper(); long jacksonTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); List<Variable> result = objectMapper.readValue(jsonString, new TypeReference<List<Variable>>() {}); long end = System.currentTimeMillis(); jacksonTotal += (end - start); } log.info("Jackson 批量解析平均耗时: {} ms", jacksonTotal / iterations); } /** * 单条解析测试 */ private static void singleParseTest(String jsonString) throws Exception { List<String> messageList = JSON.parseArray(jsonString, Variable.class).stream() .map(JSON::toJSONString) .collect(Collectors.toList()); // Hutool long hutoolTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); for (String msg : messageList) { Variable v = JSONUtil.toBean(msg, Variable.class); } long end = System.currentTimeMillis(); hutoolTotal += (end - start); } log.info("HuTool 单条解析平均耗时: {} ms", hutoolTotal / iterations); // Fastjson2 long fastjsonTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); for (String msg : messageList) { Variable v = JSON.parseObject(msg, Variable.class); } long end = System.currentTimeMillis(); fastjsonTotal += (end - start); } log.info("Fastjson2 单条解析平均耗时: {} ms", fastjsonTotal / iterations); // Gson Gson gson = new Gson(); long gsonTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); for (String msg : messageList) { Variable v = gson.fromJson(msg, Variable.class); } long end = System.currentTimeMillis(); gsonTotal += (end - start); } log.info("Gson 单条解析平均耗时: {} ms", gsonTotal / iterations); // Jackson ObjectMapper objectMapper = new ObjectMapper(); long jacksonTotal = 0; for (int i = 0; i < iterations; i++) { long start = System.currentTimeMillis(); for (String msg : messageList) { Variable v = objectMapper.readValue(msg, Variable.class); } long end = System.currentTimeMillis(); jacksonTotal += (end - start); } log.info("Jackson 单条解析平均耗时: {} ms", jacksonTotal / iterations); } /** * 循环遍历单条解析并插入集合测试 */ /** * 循环遍历单条解析并插入集合测试 (平均耗时计算) */ static void singleParseAndAddListTest(String jsonString) throws Exception { // 转换为模拟 MQ 消息体 List<String> messageList = JSON.parseArray(jsonString, Variable.class).stream() .map(JSON::toJSONString) // 将每个对象转为 JSON 字符串模拟单条消息 .collect(Collectors.toList()); // 平均耗时变量定义 double hutoolTotalTime = 0; double fastjsonTotalTime = 0; double gsonTotalTime = 0; double jacksonTotalTime = 0; // 循环 10 次计算平均耗时 for (int i = 0; i < iterations; i++) { // 1. Hutool JSONUtil 单条解析 List<Variable> hutoolList = new ArrayList<>(); long hutoolStart = System.currentTimeMillis(); for (String msg : messageList) { Variable v = JSONUtil.toBean(msg, Variable.class); hutoolList.add(v); // 将对象存入集合 } long hutoolEnd = System.currentTimeMillis(); hutoolTotalTime += (hutoolEnd - hutoolStart); // 2. Fastjson2 单条解析 List<Variable> fastjsonList = new ArrayList<>(); long fastjsonStart = System.currentTimeMillis(); for (String msg : messageList) { Variable v = JSON.parseObject(msg, Variable.class); fastjsonList.add(v); } long fastjsonEnd = System.currentTimeMillis(); fastjsonTotalTime += (fastjsonEnd - fastjsonStart); // 3. Gson 单条解析 List<Variable> gsonList = new ArrayList<>(); Gson gson = new Gson(); long gsonStart = System.currentTimeMillis(); for (String msg : messageList) { Variable v = gson.fromJson(msg, Variable.class); gsonList.add(v); } long gsonEnd = System.currentTimeMillis(); gsonTotalTime += (gsonEnd - gsonStart); // 4. Jackson 单条解析 List<Variable> jacksonList = new ArrayList<>(); ObjectMapper objectMapper = new ObjectMapper(); long jacksonStart = System.currentTimeMillis(); for (String msg : messageList) { Variable v = objectMapper.readValue(msg, Variable.class); jacksonList.add(v); } long jacksonEnd = System.currentTimeMillis(); jacksonTotalTime += (jacksonEnd - jacksonStart); } // 输出平均耗时结果 log.info("HuTool 单条解析并存入集合平均耗时: {} ms", hutoolTotalTime / iterations); log.info("Fastjson2 单条解析并存入集合平均耗时: {} ms", fastjsonTotalTime / iterations); log.info("Gson 单条解析并存入集合平均耗时: {} ms", gsonTotalTime / iterations); log.info("Jackson 单条解析并存入集合平均耗时: {} ms", jacksonTotalTime / iterations); } }4. 20W条数据、31098673字节测试结果分析(仅供参考 仅供参考 仅供参考 !!!)库名称 序列化+反序列化总耗时 性能排名Fastjson2 110ms左右 🥇 第一名Jackson 170ms左右 🥈 第二名Gson 210ms左右 🥉 第三名Hutool 1800ms左右 第四名5. 性能分析与总结测试结果分析Fastjson2性能表现: 测试结果中,无论是批量解析、逐条解析还是逐条解析并插入集合,它的速度都是最快的(30ms、39ms、39.8ms)。特性优势: 支持复杂对象结构解析,API 设计简洁高效,并修复了旧版 Fastjson 的反序列化漏洞,安全性更高。适用场景: 高并发、大数据量处理及性能敏感型应用场景。Hutool性能表现: 表现最慢,尤其在批量解析(637ms)和逐条解析(589ms)中远落后于其他库。特性优势: API 优雅轻便,开发效率高,但性能瓶颈明显,不适合超大规模数据解析。适用场景: 适合中小规模项目的快速开发,便捷性优先于性能要求的场合。Jackson性能表现: 表现较优,解析速度仅次于 Fastjson2(78ms、101ms、99.8ms),兼顾性能与功能复杂性。特性优势: 支持复杂 Java 对象和自定义配置,兼容性强,适合复杂数据结构映射需求。适用场景: 企业级应用、大型复杂系统开发及对灵活性要求高的项目。Gson性能表现: 性能优于 Hutool,但低于 Fastjson2 和 Jackson(93ms、119ms、119.5ms)。特性优势: API 简单直观,开发成本低,但在大数据量或高并发处理时性能表现不够理想。适用场景: 小规模数据解析需求,或对性能要求不高的任务。6. 推荐选择需求类型 推荐库 适用场景描述性能优先 Fastjson2 在所有测试场景中速度最快,适合高性能和高并发场景,适合日志分析、大数据处理和消息队列数据解析等应用。轻量便捷 Hutool 更适合中小规模项目的快速开发,虽然性能较低,但 API 优雅轻便,适合便捷性优先的场合。功能复杂需求 Jackson 兼顾性能与灵活性,支持复杂数据结构和自定义配置,适合大型企业级应用或复杂对象映射需求。简单解析需求 Gson API 简洁易用,适合小规模数据解析任务,学习成本低,但不适合大数据和高并发需求。7. 结论与建议Fastjson2:性能最高,适合高并发与大数据处理需求。安全性较高,是性能敏感应用的首选。Hutool:开发便捷但性能较低,适合中小规模项目。如果对开发效率要求高且数据量适中,可以选择它。Jackson:性能与灵活性兼顾,适合复杂对象和企业级系统。面向需要自定义解析规则的场景表现更出色。Gson:简单易用但性能低于 Fastjson2 和 Jackson。适合小型项目或对性能要求不高的场合。注意事项安全性: Fastjson2 安全性最佳,其他库需关注版本更新,以避免反序列化漏洞。兼容性: Jackson 在跨平台兼容性和复杂结构处理方面表现更佳。性能评估: 项目正式使用前,应基于实际生产环境进行更大规模的性能测试和压力测试。仅当前测试数据明确显示:Fastjson2 性能最佳,适合高性能需求。Hutool 性能最慢,更适合便捷开发而非大规模数据解析。Jackson 在性能和灵活性之间取得平衡,适合复杂应用场景。Gson 表现优于 Hutool,但略逊于 Fastjson2 和 Jackson,适合轻量级需求。————————————————原文链接:https://blog.csdn.net/lps12345666/article/details/144931886
-
1. 抽象类1.1 概述1.1.1 抽象类引入 父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,父类可能知道子类应该有哪个功能,但是功能具体怎么实现父类是不清楚的(由子类自己决定),父类只需要提供一个没有方法体的定义即可,具体实现交给子类自己去实现。我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。 抽象方法 : 没有方法体的方法。抽象类:包含抽象方法的类。1.2 abstract使用格式abstract是抽象的意思,用于修饰方法方法和类,修饰的方法是抽象方法,修饰的类是抽象类。 1.2.1 抽象方法使用abstract 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。 定义格式: 修饰符 abstract 返回值类型 方法名 (参数列表); 代码举例: public abstract void run(); 1.2.2 抽象类如果一个类包含抽象方法,那么该类必须是抽象类。注意:抽象类不一定有抽象方法,但是有抽象方法的类必须定义成抽象类。 定义格式: abstract class 类名字 { } 代码举例: public abstract class Animal { public abstract void run();} 1.2.3 抽象类的使用要求:继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。 代码举例: // 父类,抽象类abstract class Employee {private String id;private String name;private double salary; public Employee() {} public Employee(String id, String name, double salary) {this.id = id;this.name = name;this.salary = salary;} // 抽象方法// 抽象方法必须要放在抽象类中abstract public void work();} // 定义一个子类继承抽象类class Manager extends Employee {public Manager() {}public Manager(String id, String name, double salary) {super(id, name, salary);}// 2.重写父类的抽象方法@Overridepublic void work() {System.out.println("管理其他人");}} // 定义一个子类继承抽象类class Cook extends Employee {public Cook() {}public Cook(String id, String name, double salary) {super(id, name, salary);}@Overridepublic void work() {System.out.println("厨师炒菜多加点盐...");}} // 测试类public class Demo10 {public static void main(String[] args) {// 创建抽象类,抽象类不能创建对象// 假设抽象类让我们创建对象,里面的抽象方法没有方法体,无法执行.所以不让我们创建对象// Employee e = new Employee();// e.work(); // 3.创建子类Manager m = new Manager();m.work(); Cook c = new Cook("ap002", "库克", 1);c.work();}} 此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法。 1.3 抽象类的特征抽象类的特征总结起来可以说是 有得有失 有得:抽象类得到了拥有抽象方法的能力。 有失:抽象类失去了创建对象的能力。 其他成员(构造方法,实例方法,静态方法等)抽象类都是具备的。 1.4 抽象类的细节不需要背,只要当idea报错之后,知道如何修改即可。 关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。 理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。 理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。 理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类,编译无法通过而报错。 理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。 抽象类存在的意义是为了被子类继承。 理解:抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。 1.5 抽象类存在的意义 抽象类存在的意义是为了被子类继承,否则抽象类将毫无意义。抽象类可以强制让子类,一定要按照规定的格式进行重写。 2. 接口2.1 概述我们已经学完了抽象类,抽象类中可以用抽象方法,也可以有普通方法,构造方法,成员变量等。那么什么是接口呢?接口是更加彻底的抽象,JDK7之前,包括JDK7,接口中全部是抽象方法。接口同样是不能创建对象的。 2.2 定义格式//接口的定义格式:interface 接口名称{ // 抽象方法} // 接口的声明:interface// 接口名称:首字母大写,满足“驼峰模式” 2.3 接口成分的特点在JDK7,包括JDK7之前,接口中的只有包含:抽象方法和常量 2.3.1.抽象方法 注意:接口中的抽象方法默认会自动加上public abstract修饰程序员无需自己手写!! 按照规范:以后接口中的抽象方法建议不要写上public abstract。因为没有必要啊,默认会加上。 2.3.2 常量在接口中定义的成员变量默认会加上: public static final修饰。也就是说在接口中定义的成员变量实际上是一个常量。这里是使用public static final修饰后,变量值就不可被修改,并且是静态化的变量可以直接用接口名访问,所以也叫常量。常量必须要给初始值。常量命名规范建议字母全部大写,多个单词用下划线连接。 2.3.3 案例演示public interface InterF { // 抽象方法! // public abstract void run(); void run(); // public abstract String getName(); String getName(); // public abstract int add(int a , int b); int add(int a , int b); // 它的最终写法是: // public static final int AGE = 12 ; int AGE = 12; //常量 String SCHOOL_NAME = "百度"; } 2.4 基本的实现2.4.1 实现接口的概述类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。 2.4.2 实现接口的格式/**接口的实现: 在Java中接口是被实现的,实现接口的类称为实现类。 实现类的格式:*/class 类名 implements 接口1,接口2,接口3...{ } 从上面格式可以看出,接口是可以被多实现的。大家可以想一想为什么呢? 2.4.3 类实现接口的要求和意义必须重写实现的全部接口中所有抽象方法。如果一个类实现了接口,但是没有重写完全部接口的全部抽象方法,这个类也必须定义成抽象类。意义:接口体现的是一种规范,接口对实现类是一种强制性的约束,要么全部完成接口申明的功能,要么自己也定义成抽象类。这正是一种强制性的规范。2.4.4 类与接口基本实现案例假如我们定义一个运动员的接口(规范),代码如下: /** 接口:接口体现的是规范。 * */public interface SportMan { void run(); // 抽象方法,跑步。 void law(); // 抽象方法,遵守法律。 String compittion(String project); // 抽象方法,比赛。} 接下来定义一个乒乓球运动员类,实现接口,实现接口的实现类代码如下: package com.itheima._03接口的实现;/** * 接口的实现: * 在Java中接口是被实现的,实现接口的类称为实现类。 * 实现类的格式: * class 类名 implements 接口1,接口2,接口3...{ * * * } * */public class PingPongMan implements SportMan { @Override public void run() { System.out.println("乒乓球运动员稍微跑一下!!"); } @Override public void law() { System.out.println("乒乓球运动员守法!"); } @Override public String compittion(String project) { return "参加"+project+"得金牌!"; }} 测试代码: public class TestMain { public static void main(String[] args) { // 创建实现类对象。 PingPongMan zjk = new PingPongMan(); zjk.run(); zjk.law(); System.out.println(zjk.compittion("全球乒乓球比赛")); }} 2.4.5 类与接口的多实现案例类与接口之间的关系是多实现的,一个类可以同时实现多个接口。 首先我们先定义两个接口,代码如下: /** 法律规范:接口*/public interface Law { void rule();} /** 这一个运动员的规范:接口*/public interface SportMan { void run();} 然后定义一个实现类: /** * Java中接口是可以被多实现的: * 一个类可以实现多个接口: Law, SportMan * * */public class JumpMan implements Law ,SportMan { @Override public void rule() { System.out.println("尊长守法"); } @Override public void run() { System.out.println("训练跑步!"); }} 从上面可以看出类与接口之间是可以多实现的,我们可以理解成实现多个规范,这是合理的。 2.5 接口与接口的多继承Java中,接口与接口之间是可以多继承的:也就是一个接口可以同时继承多个接口。大家一定要注意: 类与接口是实现关系 接口与接口是继承关系 接口继承接口就是把其他接口的抽象方法与本接口进行了合并。 案例演示: public interface Abc { void go(); void test();} /** 法律规范:接口*/public interface Law { void rule(); void test();} * * 总结: * 接口与类之间是多实现的。 * 接口与接口之间是多继承的。 * */public interface SportMan extends Law , Abc { void run();} 2.6扩展:接口的细节不需要背,只要当idea报错之后,知道如何修改即可。 关于接口的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。 当两个接口中存在相同抽象方法的时候,该怎么办?只要重写一次即可。此时重写的方法,既表示重写1接口的,也表示重写2接口的。 实现类能不能继承A类的时候,同时实现其他接口呢?继承的父类,就好比是亲爸爸一样实现的接口,就好比是干爹一样可以继承一个类的同时,再实现多个接口,只不过,要把接口里面所有的抽象方法,全部实现。 实现类能不能继承一个抽象类的时候,同时实现其他接口呢?实现类可以继承一个抽象类的同时,再实现其他多个接口,只不过要把里面所有的抽象方法全部重写。 实现类Zi,实现了一个接口,还继承了一个Fu类。假设在接口中有一个方法,父类中也有一个相同的方法。子类如何操作呢?处理办法一:如果父类中的方法体,能满足当前业务的需求,在子类中可以不用重写。处理办法二:如果父类中的方法体,不能满足当前业务的需求,需要在子类中重写。 如果一个接口中,有10个抽象方法,但是我在实现类中,只需要用其中一个,该怎么办?可以在接口跟实现类中间,新建一个中间类(适配器类)让这个适配器类去实现接口,对接口里面的所有的方法做空重写。让子类继承这个适配器类,想要用到哪个方法,就重写哪个方法。因为中间类没有什么实际的意义,所以一般会把中间类定义为抽象的,不让外界创建对象 3. 内部类3.1 概述3.1.1 什么是内部类将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类。可以把内部类理解成寄生,外部类理解成宿主。 3.1.2 什么时候使用内部类一个事物内部还有一个独立的事物,内部的事物脱离外部的事物无法独立使用 人里面有一颗心脏。汽车内部有一个发动机。为了实现更好的封装性。3.2 内部类的分类按定义的位置来分 成员内部内,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)局部内部类,类定义在方法内匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外。3.3 成员内部类成员内部类特点: 无static修饰的内部类,属于外部类对象的。宿主:外部类对象。内部类的使用格式: 外部类.内部类。 // 访问内部类的类型都是用 外部类.内部类 获取成员内部类对象的两种方式: 方式一:外部直接创建成员内部类的对象 外部类.内部类 变量 = new 外部类().new 内部类(); 方式二:在外部类中定义一个方法提供内部类的对象 案例演示 方式一:public class Test { public static void main(String[] args) { // 宿主:外部类对象。 // Outer out = new Outer(); // 创建内部类对象。 Outer.Inner oi = new Outer().new Inner(); oi.method(); }} class Outer { // 成员内部类,属于外部类对象的。 // 拓展:成员内部类不能定义静态成员。 public class Inner{ // 这里面的东西与类是完全一样的。 public void method(){ System.out.println("内部类中的方法被调用了"); } }} 方式二:public class Outer { String name; private class Inner{ static int a = 10; } public Inner getInstance(){ return new Inner(); }} public class Test { public static void main(String[] args) { Outer o = new Outer(); System.out.println(o.getInstance()); }} 3.4 成员内部类的细节编写成员内部类的注意点: 成员内部类可以被一些修饰符所修饰,比如: private,默认,protected,public,static等在成员内部类里面,JDK16之前不能定义静态变量,JDK16开始才可以定义静态变量。创建内部类对象时,对象中有一个隐含的Outer.this记录外部类对象的地址值。(请参见3.6节的内存图)详解: 内部类被private修饰,外界无法直接获取内部类的对象,只能通过3.3节中的方式二获取内部类的对象 被其他权限修饰符修饰的内部类一般用3.3节中的方式一直接获取内部类的对象 内部类被static修饰是成员内部类中的特殊情况,叫做静态内部类下面单独学习。 内部类如果想要访问外部类的成员变量,外部类的变量必须用final修饰,JDK8以前必须手动写final,JDK8之后不需要手动写,JDK默认加上。 3.5 成员内部类面试题请在?地方向上相应代码,以达到输出的内容 注意:内部类访问外部类对象的格式是:外部类名.this public class Test { public static void main(String[] args) { Outer.inner oi = new Outer().new inner(); oi.method(); }} class Outer { // 外部类 private int a = 30; // 在成员位置定义一个类 class inner { private int a = 20; public void method() { int a = 10; System.out.println(???); // 10 答案:a System.out.println(???); // 20 答案:this.a System.out.println(???); // 30 答案:Outer.this.a } }} 3.6 成员内部类内存图 3.7 静态内部类静态内部类特点: 静态内部类是一种特殊的成员内部类。有static修饰,属于外部类本身的。总结:静态内部类与其他类的用法完全一样。只是访问的时候需要加上外部类.内部类。拓展1:静态内部类可以直接访问外部类的静态成员。拓展2:静态内部类不可以直接访问外部类的非静态成员,如果要访问需要创建外部类的对象。拓展3:静态内部类中没有银行的Outer.this。内部类的使用格式: 外部类.内部类。 静态内部类对象的创建格式: 外部类.内部类 变量 = new 外部类.内部类构造器; 调用方法的格式: 调用非静态方法的格式:先创建对象,用对象调用调用静态方法的格式:外部类名.内部类名.方法名();案例演示: // 外部类:Outer01class Outer01{ private static String sc_name = "黑马程序"; // 内部类: Inner01 public static class Inner01{ // 这里面的东西与类是完全一样的。 private String name; public Inner01(String name) { this.name = name; } public void showName(){ System.out.println(this.name); // 拓展:静态内部类可以直接访问外部类的静态成员。 System.out.println(sc_name); } }} public class InnerClassDemo01 { public static void main(String[] args) { // 创建静态内部类对象。 // 外部类.内部类 变量 = new 外部类.内部类构造器; Outer01.Inner01 in = new Outer01.Inner01("张三"); in.showName(); }} 3.8 局部内部类局部内部类 :定义在方法中的类。定义格式: class 外部类名 {数据类型 变量名; 修饰符 返回值类型 方法名(参数列表) {// …class 内部类 {// 成员变量// 成员方法}}} 3.9 匿名内部类【重点】3.9.1 概述匿名内部类 :是内部类的简化写法。他是一个隐含了名字的内部类。开发中,最常用到的内部类就是匿名内部类了。 3.9.2 格式new 类名或者接口名() { 重写方法;}; 包含了: 继承或者实现关系 方法重写 创建对象 所以从语法上来讲,这个整体其实是匿名内部类对象 3.9.2 什么时候用到匿名内部类实际上,如果我们希望定义一个只要使用一次的类,就可考虑使用匿名内部类。匿名内部类的本质作用 是为了简化代码。 之前我们使用接口时,似乎得做如下几步操作: 定义子类重写接口中的方法创建子类对象调用重写后的方法interface Swim { public abstract void swimming();} // 1. 定义接口的实现类class Student implements Swim { // 2. 重写抽象方法 @Override public void swimming() { System.out.println("狗刨式..."); }} public class Test { public static void main(String[] args) { // 3. 创建实现类对象 Student s = new Student(); // 4. 调用方法 s.swimming(); }} 我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。 3.9.3 匿名内部类前提和格式匿名内部类必须继承一个父类或者实现一个父接口。 匿名内部类格式 new 父类名或者接口名(){ // 方法重写 @Override public void method() { // 执行语句 }}; 3.9.4 使用方式以接口为例,匿名内部类的使用,代码如下: interface Swim { public abstract void swimming();} public class Demo07 { public static void main(String[] args) { // 使用匿名内部类new Swim() {@Overridepublic void swimming() {System.out.println("自由泳...");}}.swimming(); // 接口 变量 = new 实现类(); // 多态,走子类的重写方法 Swim s2 = new Swim() { @Override public void swimming() { System.out.println("蛙泳..."); } }; s2.swimming(); s2.swimming(); }} 3.9.5 匿名内部类的特点定义一个没有名字的内部类这个类实现了父类,或者父类接口匿名内部类会创建这个没有名字的类的对象3.9.6 匿名内部类的使用场景通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。———————————————— 原文链接:https://blog.csdn.net/fj123789/article/details/145681640
-
文章目录类和对象(下)八、static 关键字8.1 静态变量8.1.1 概念8.1.2 访问方式8.2 静态方法8.2.1 概念8.2.2 访问限制8.3 static成员变量初始化8.4 静态成员的使用场景九、代码块8.1 代码块概念以及分类8.2 普通代码块(局部代码块)8.3 构造代码块(实例代码块)8.3.1 执行时机8.3.2 作用和特点8.4 静态代码块(静态初始化块)8.4.1 执行时机8.4.2 使用场景8.4.3 示例8.4.4 注意事项8.5 同步代码块(了解)十、内部类10.1 内部类概述10.2 成员内部类10.2.1 基本语法10.2.2 访问规则10.3 静态内部类10.3.1 基本语法10.3.2 访问方式10.4 局部内部类10.4.1 特点10.5 匿名内部类(抽象类和接口时详细介绍)10.5.1 基本写法10.5.2 特点10.6 小结十一、对象的打印11.1 Object 的 toString()11.2 重写 toString()11.2.1 基本示例11.2.2 重写要点11.3 打印数组11.4 小结十二、总结与展望类和对象(下)欢迎讨论:如果你对本篇内容有任何疑问或想深入探讨,欢迎在评论区留言交流!点赞、收藏与分享:觉得内容有帮助就请点赞、收藏并分享给更多学习Java的小伙伴! 继续学习之旅:本篇文章将详细讲解 static 关键字、代码块以及内部类的相关概念和使用,让你在面向对象的世界中更进一步,玩转 Java 的高级特性!八、static 关键字static(静态)是 Java 中的一个关键字,主要用于修饰类中的变量或方法。被 static 修饰的成员属于 类本身 而非某个实例对象。它在编译阶段就会被加载到方法区中(Java 8 之后元空间/Metaspace),无需通过创建对象就能访问。其生命周期伴随类的一生(即:随类的加载而创建,随类的卸载而销毁)8.1 静态变量8.1.1 概念非静态变量:也称为实例变量,每创建一个对象就会产生一份新的变量副本。静态变量:也称为类变量,只有一份共享的副本,所有该类对象都可以访问和修改。public class Counter { public static int count = 0; // 静态变量 public int id; // 实例变量 public Counter() { count++; this.id = count; } public static void main(String[] args) { Counter c1 = new Counter(); Counter c2 = new Counter(); System.out.println(Counter.count); // 2 System.out.println(c1.id); // 1 System.out.println(c2.id); // 2 }}count 为静态变量,全类共享;id 为实例变量,每个对象拥有自己的 id 值。8.1.2 访问方式类名.静态变量(推荐)对象名.静态变量(不推荐,容易引起误解)System.out.println(Counter.count); // 推荐System.out.println(c1.count); // 不推荐8.2 静态方法8.2.1 概念被 static 修饰的方法属于类本身。无需创建对象,就可以通过 类名.方法名() 的方式直接调用。在静态方法中,不能 直接访问非静态成员(因为实例成员依赖于对象的存在,而静态方法执行时可能尚未创建任何对象)。public class MathUtil { public static int add(int a, int b) { return a + b; } public static void main(String[] args) { int sum = MathUtil.add(3, 5); System.out.println(sum); // 8 }}8.2.2 访问限制静态方法中只能访问 静态成员;非静态方法中则可以同时访问静态成员和非静态成员。8.3 static成员变量初始化注意:静态成员变量一般不会在构造方法中进行初始化,因为构造方法与对象实例关联,而静态成员变量则属于类本身。静态成员变量的初始化有两种方式:就地初始化:在定义时直接给出初值静态代码块初始化:在 static { ... } 代码块中完成赋值(下文再讲)就地初始化示例代码:public class Student { private String name; private String gender; private int age; // 静态成员变量就地初始化 private static String classRoom = "Bit36"; // 构造方法、普通方法、get/set方法等 // ...}8.4 静态成员的使用场景工具类/工具方法:如 Math 类的 sqrt()、abs() 等,方便直接通过类名调用。单例模式:通过静态字段持有唯一实例。计数器:统计对象数量或方法调用次数。常量池:public static final 定义常量,方便全局使用且避免重复创建。九、代码块8.1 代码块概念以及分类定义:在 Java 中使用 {} 包裹的一段或多段代码,即可称为“代码块”。常见分类:普通(局部)代码块:最常见,出现在方法体内部、流程控制语句中等,用来限制局部变量作用域或组织逻辑。构造代码块(实例代码块):定义在类中、方法外,不带 static,在构造方法执行之前运行,每次创建对象都会执行。静态代码块(静态初始化块):使用 static {} 修饰,类加载时(只一次)执行,用来初始化静态成员或做只需执行一次的操作。同步代码块:用 synchronized(锁对象) { ... } 来实现多线程同步,控制临界区内的线程安全,(入门只需了解)。8.2 普通代码块(局部代码块)普通代码块通常指在方法或语句块内部,用花括号 {} 包裹的那部分代码。它的主要功能是局部作用域的划分。示例:public class Main { public static void main(String[] args) { // 普通(局部)代码块 { int x = 10; System.out.println("x = " + x); } // x 的作用域仅限于上面的 {} 之内 // System.out.println(x); // 编译报错 // 再次声明一个同名变量 x,不会冲突 int x = 100; System.out.println("x2 = " + x); }}输出:x = 10x2 = 100要点:普通代码块会限制局部变量的作用范围。常用于封装临时逻辑或缩短变量生命周期,防止与其他同名变量冲突或占用资源太久。8.3 构造代码块(实例代码块)构造代码块(又称“实例初始化块”)是指在类中、方法外,但没有 static 修饰的一段 {} 代码。它在创建对象时会先于构造方法执行,用来对实例成员进行初始化或执行公共逻辑。从先于构造函数体执行的思想上看和C++的初始化列表有相似之处。8.3.1 执行时机每次调用 new 构造对象时,都会先执行构造代码块,然后再执行构造方法。如果有多个构造方法,则不管调用哪一个,都会先执行这段构造代码块。示例:public class Student { private String name; private int age; // 构造代码块(实例代码块) { this.name = "bit"; this.age = 12; System.out.println("I am instance init()!"); } public Student() { System.out.println("I am Student init()!"); } public void show() { System.out.println("name: " + name + " age: " + age); } public static void main(String[] args) { Student stu = new Student(); stu.show(); }}输出:I am instance init()!I am Student init()!name: bit age: 12可以看到,构造代码块先于构造方法执行。8.3.2 作用和特点作用:可在对象创建前做一些实例变量的初始化或公共操作。执行次数:与 new 对象的次数相同,每次创建对象都会执行一次。与构造方法的关系:若有多个构造方法,可将公共初始化操作放到构造代码块,避免重复代码。执行顺序:构造代码块 → 构造方法。8.4 静态代码块(静态初始化块)静态代码块使用 static {} 修饰,是属于类级别的初始化逻辑。它会在类加载的时候执行一次,不随着对象创建反复执行。8.4.1 执行时机类第一次加载时,JVM 执行所有静态代码块。只执行一次,与后续创建多少对象无关。8.4.2 使用场景初始化静态成员变量。进行一次性的操作(如注册驱动、加载配置等)。8.4.3 示例public class Student { private String name; private int age; private static String classRoom; // 静态代码块 static { classRoom = "bit306"; System.out.println("I am static init()!"); } // 构造代码块 { this.name = "bit"; this.age = 12; System.out.println("I am instance init()!"); } public Student() { System.out.println("I am Student init()!"); } public static void main(String[] args) { System.out.println("----开始 main 方法----"); Student s1 = new Student(); Student s2 = new Student(); }}输出顺序示例:I am static init()!----开始 main 方法----I am instance init()!I am Student init()!I am instance init()!I am Student init()!8.4.4 注意事项静态代码块只执行一次,无论创建多少对象都不会重复执行。如果在同一个类里定义多个 static {},会按照代码顺序依次执行。(即合并)静态环境下无法访问实例成员;要访问实例变量或实例方法,需要先创建对象。8.5 同步代码块(了解)“同步代码块”并不是为了初始化而存在,而是为了解决多线程并发访问同一资源时的线程安全问题。写法一般是:synchronized(锁对象) { // 需要线程同步的代码}当一个线程进入该代码块并持有“锁对象”时,其他线程只能等待,直到该线程执行完毕并释放锁。通常“锁对象”可用 this(当前实例)、某个类对象、或专门的锁实例等。十、内部类内部类(Inner Class)是将一个类的定义放在另一个类的内部,从而形成逻辑上的隶属关系。Java 提供了多种内部类形式,包括成员内部类、静态内部类、局部内部类以及匿名内部类。通过内部类,我们可以更好地封装和管理代码结构,也能直接访问外部类的私有成员,增强代码的灵活性和可读性。10.1 内部类概述定义:在一个类的内部再定义一个类(或接口),该类称之为“内部类”或“嵌套类”。分类:成员内部类(非静态内部类)静态内部类局部内部类(定义在方法或代码块内)匿名内部类(没有类名,直接定义并实例化)好处:内部类可以直接访问外部类的成员(包括私有成员),从而简化了访问操作。逻辑上隶属关系更清晰,起到封装与隐藏的作用。通过匿名内部类等方式,可以使代码更简洁,尤其在回调或事件监听等场景中。10.2 成员内部类成员内部类又叫非静态内部类,它是定义在外部类的成员位置(与外部类的成员变量、方法同级)但不带 static 关键字的内部类。10.2.1 基本语法public class Outer { private String name = "OuterName"; // 成员内部类 public class Inner { public void show() { // 直接访问外部类的私有成员 System.out.println("Outer name: " + name); } } public void test() { Inner inner = new Inner(); inner.show(); } public static void main(String[] args) { Outer outer = new Outer(); outer.test(); }}执行流程:创建 Outer 对象:Outer outer = new Outer();在 outer.test() 方法中,实例化 Inner:Inner inner = new Inner();调用 inner.show(),可以直接访问 Outer 类中的 name。10.2.2 访问规则内部类可以直接访问外部类的所有成员(包括 private)。若内部类成员与外部类成员同名,可用 外部类名.this.成员 的方式区分,例如 Outer.this.name。在外部类的非静态方法中,可以直接创建内部类实例;在外部类的静态方法或其他类中,则需通过 外部类对象.new 内部类构造() 来创建。10.3 静态内部类静态内部类,也称静态嵌套类,使用 static 修饰。它与成员内部类的主要区别在于:静态内部类只能访问外部类的静态成员,无法直接访问外部类的非静态成员。创建静态内部类的对象时,不需要外部类对象的实例。10.3.1 基本语法public class Outer { private String name = "OuterName"; private static String staticName = "StaticOuterName"; // 静态内部类 public static class Inner { public void show() { // 只能直接访问外部类的静态成员 System.out.println("Outer staticName: " + staticName); // System.out.println(name); // 非静态成员,无法直接访问 } } public static void main(String[] args) { // 不需要外部类对象,直接创建静态内部类对象 Outer.Inner inner = new Outer.Inner(); inner.show(); }}10.3.2 访问方式静态内部类对象的创建方式:外部类名.内部类名 对象名 = new 外部类名.内部类名();静态内部类中的实例方法,依然需要创建内部类实例来调用;但如果有静态方法或静态变量,可以通过 Outer.Inner.静态方法 或 Outer.Inner.静态变量 直接访问。10.4 局部内部类局部内部类是定义在方法体或代码块内部的类,只在该方法或代码块中可见和使用。局部内部类可以看作“更局部化”的内部类,常用于一些只在某个方法中使用的场景。public class Outer { public void method() { class Inner { public void show() { System.out.println("I am local inner class!"); } } // 在方法内部创建并使用 Inner inner = new Inner(); inner.show(); } public static void main(String[] args) { new Outer().method(); }}10.4.1 特点作用域限制:只能在定义它的方法或代码块中创建并使用。访问外部变量:可以访问外部类的成员,也可以访问该方法中被 final 或“事实上的最终”变量所修饰的局部变量(Java 8+ 开始,只要不修改该变量即可,不必显式 final)。10.5 匿名内部类(抽象类和接口时详细介绍)匿名内部类(Anonymous Inner Class)没有类名,通常用于简化创建某些接口或抽象类子类对象的过程,尤其在回调、事件处理等场景中使用广泛。10.5.1 基本写法interface ITest { void func();}public class Demo { public static void main(String[] args) { // 匿名内部类:直接 new 接口(或抽象类),然后立刻重写其中的方法 ITest test = new ITest() { @Override public void func() { System.out.println("Anonymous Inner Class func"); } }; test.func(); }}new 接口/抽象类() { ... }:创建一个实现该接口或继承该抽象类的匿名子类对象。匿名:没有类名,直接在此处定义并实例化。10.5.2 特点只能使用一次:若需要多次创建同样功能的对象,通常还是单独定义一个类或使用 Lambda(对于函数式接口)更好。简化代码:不用显式定义一个实现类/子类。10.6 小结成员内部类:需要先创建外部类对象,再通过 外部类对象.new 内部类() 来实例化。可以访问外部类的所有成员。静态内部类:使用 static 修饰,只能直接访问外部类的静态成员。无需外部类实例即可创建,使用 外部类名.内部类名 方式。局部内部类:定义在方法或代码块内部,仅在该方法/代码块中可见。可以访问外部类的成员,也可以访问方法内“事实上的最终”变量。匿名内部类:没有类名,常用于简化接口或抽象类的实现。使用场景多在回调、事件监听等。掌握内部类的使用场景与写法,可以使代码的封装性更好,也能让某些实现方式更灵活、更简洁。在实际开发中,根据需求选择合适的内部类形式:若需要频繁调用且功能独立,建议成员内部类或静态内部类;若仅在某个方法中临时使用,且逻辑不复杂,可选择局部内部类或匿名内部类来简化代码。十一、对象的打印在 Java 中,当我们使用 System.out.println(obj); 或字符串拼接(如 "" + obj)来打印一个对象时,实质上是自动调用该对象的 toString() 方法。如果类中没有重写 toString(),则默认会调用 Object 类的 toString() 方法,打印出类似 类名@哈希值 的信息(如 com.bit.demo.Student@3f99bd52),往往并不是我们想要的“可读输出”。11.1 Object 的 toString()默认实现:Object 的 toString() 返回的字符串格式一般是:getClass().getName() + "@" + Integer.toHexString(hashCode())也就是“类的完整名称@哈希码”形式,便于识别对象在内存中的“标识”,但并不展示对象的具体属性信息。为什么常被打印?在使用 System.out.println(对象引用); 或字符串拼接时,会自动调用 toString()。如果没有重写该方法,就会使用 Object 的默认实现。11.2 重写 toString()为了打印出更有意义的信息,我们通常会在自定义的类中重写(Override) toString() 方法。这样当打印对象时,就能输出该对象的关键属性值或其他说明性内容。11.2.1 基本示例public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } // 重写 toString() 方法 @Override public String toString() { return "Student{name='" + name + "', age=" + age + "}"; } public static void main(String[] args) { Student stu = new Student("Alice", 20); System.out.println(stu); // 自动调用 stu.toString() => 输出: Student{name='Alice', age=20} }}11.2.2 重写要点方法签名:必须是 public String toString(),且带有 @Override 注解(可选但建议)。返回值:返回一个可读性强、能够体现对象核心信息的字符串。风格多样:可以根据需要自定义输出格式,或使用 JSON、XML 等形式。IDE 快捷生成:大多数 IDE(如 Eclipse、IntelliJ IDEA)可以自动生成 toString() 代码,方便使用。11.3 打印数组打印数组对象时,如果使用 System.out.println(arr); 也会得到类似 [Ljava.lang.String;@1540e19d 这样的结果(同样是 Object 的默认 toString())。如果想查看数组元素,可以使用以下方式:Arrays.toString(数组):适用于一维数组。String[] arr = {"Hello", "World"};System.out.println(Arrays.toString(arr)); // [Hello, World]Arrays.deepToString(数组):适用于多维数组。String[][] arr2D = {{"A", "B"}, {"C", "D"}};System.out.println(Arrays.deepToString(arr2D)); // [[A, B], [C, D]]11.4 小结默认打印对象:调用 Object.toString(),返回“类名@哈希值”,可读性差。重写 toString():通过在自定义类中重写 toString(),让打印对象时输出更有意义的属性信息。打印数组:使用 Arrays.toString() 或 Arrays.deepToString() 更好地展示数组内容。在实际开发中,重写 toString() 不仅方便调试与日志记录,也能让我们更直观地了解对象的核心数据。合理使用 toString() 让输出信息更友好,对日常开发帮助很大。十二、总结与展望本篇内容主要围绕以下三个方面展开:static关键字静态变量和静态方法在类级别上提供共享资源和操作,无需创建对象即可使用。静态代码块为类提供一次性初始化的机会。代码块包括静态代码块、实例代码块等,用于在不同阶段进行初始化操作。掌握代码块的执行顺序有助于理解对象的生命周期。内部类提供了更灵活的访问方式和封装机制。主要分为成员内部类、静态内部类、匿名内部类和局部内部类。未来的学习方向Java 继承与多态:继续探索面向对象的另外两个特性,理解类与类之间的继承关系以及动态绑定机制。接口与抽象类:深入理解接口与抽象类的应用场景,以及如何运用多态思想进行程序扩展。常用设计模式:结合内部类与静态特性,在单例、工厂、观察者等常见模式中,静态和内部类都有独特的用武之地。———————————————— 原文链接:https://blog.csdn.net/2301_79849925/article/details/146019987
-
Java对接微信支付全过程详解引言在数字化商业蓬勃发展的今天,移动支付已成为连接用户与服务的核心纽带。微信支付凭借其庞大的用户基数和成熟的生态体系,成为企业拓展线上业务不可或缺的支付工具。然而,对于Java开发者而言,如何高效、安全地对接微信支付接口,构建稳定可靠的支付系统,仍面临诸多技术挑战——从复杂的签名验签机制到异步通知的幂等性处理,从证书管理到高并发场景下的性能优化,每一步都需要精准的技术设计与严谨的代码实现。本文将以系统性、实战性为导向,深入剖析Java对接微信支付的核心流程与关键技术。无论是Native支付、JSAPI支付还是小程序支付,其底层逻辑均围绕预支付订单生成、支付结果异步通知、订单状态主动查询三大核心环节展开。文章不仅提供清晰的代码示例(基于Spring Boot框架与微信支付V3 API),更聚焦于实际开发中的高频痛点:如何通过RSA签名保障通信安全?如何设计幂等回调接口避免重复扣款?如何利用微信平台证书防止伪造请求?这些问题将在文中逐一击破。此外,本文还将探讨企业级支付系统中的最佳实践,例如使用WechatPay Apache HttpClient简化证书管理、通过分布式锁实现订单状态同步、结合日志监控提升系统可观测性等。无论您是初探支付领域的开发者,还是需优化现有支付架构的技术负责人,均可从中获得从基础配置到高阶优化的完整知识链路,助力构建高可用、高安全的支付服务体系,为业务增长筑牢技术基石。一、准备工作注册微信商户平台获取商户号(mchid)、API密钥(API_KEY)。下载API证书(apiclient_cert.pem 和 apiclient_key.pem)。配置支付参数# application.propertieswxpay.mch-id=你的商户号wxpay.api-key=你的API密钥wxpay.notify-url=https://你的域名/api/wxpay/notify二、核心接口实现1. 生成预支付订单(Native支付)public class WxPayService { private String mchId; private String apiKey; private String notifyUrl; // 初始化参数(通过@Value注入或配置文件读取) /** * 生成Native支付二维码链接 */ public String createNativeOrder(String orderId, int amount) throws Exception { String url = "https://api.mch.weixin.qq.com/v3/pay/transactions/native"; Map<String, Object> params = new HashMap<>(); params.put("mchid", mchId); params.put("appid", "你的AppID"); // 如果是公众号/小程序支付 params.put("description", "订单描述"); params.put("out_trade_no", orderId); params.put("notify_url", notifyUrl); params.put("amount", Map.of("total", amount, "currency", "CNY")); // 生成签名并发送请求 String body = JSON.toJSONString(params); String authorization = generateSignature("POST", url, body); // 使用OkHttp或RestTemplate发送请求 String response = sendPostRequest(url, body, authorization); return JSON.parseObject(response).getString("code_url"); } /** * 生成V3接口的Authorization头 */ private String generateSignature(String method, String url, String body) { String timestamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = UUID.randomUUID().toString().replace("-", ""); String message = method + "\n" + url + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n"; String signature = signWithSHA256RSA(message, apiKey); // 使用私钥签名 return "WECHATPAY2-SHA256-RSA2048 " + "mchid=\"" + mchId + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," + "serial_no=\"你的证书序列号\"," + "signature=\"" + signature + "\""; }}2. 处理微信支付回调(关键!)@RestController@RequestMapping("/api/wxpay")public class WxPayCallbackController { @PostMapping("/notify") public String handleNotify(@RequestBody String requestBody, @RequestHeader("Wechatpay-Signature") String signature, @RequestHeader("Wechatpay-Timestamp") String timestamp, @RequestHeader("Wechatpay-Nonce") String nonce) { // 1. 验证签名(防止伪造请求) String message = timestamp + "\n" + nonce + "\n" + requestBody + "\n"; boolean isValid = verifySignature(message, signature, publicKey); // 使用微信平台公钥验证 if (!isValid) { return "<xml><return_code>FAIL</return_code></xml>"; } // 2. 解析回调数据 Map<String, String> result = parseXml(requestBody); String orderId = result.get("out_trade_no"); String transactionId = result.get("transaction_id"); // 3. 幂等性处理:检查订单是否已处理 if (orderService.isOrderPaid(orderId)) { return "<xml><return_code>SUCCESS</return_code></xml>"; } // 4. 更新订单状态 orderService.updateOrderToPaid(orderId, transactionId); // 5. 返回成功响应(必须!否则微信会重试) return "<xml><return_code>SUCCESS</return_code></xml>"; }}3. 查询订单状态public class WxPayService { public Map<String, String> queryOrder(String orderId) throws Exception { String url = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/" + orderId + "?mchid=" + mchId; String authorization = generateSignature("GET", url, ""); String response = sendGetRequest(url, authorization); return JSON.parseObject(response, Map.class); }}三、关键注意事项签名验证所有回调必须验证签名,防止伪造请求。使用微信提供的平台证书验证。幂等性设计通过数据库唯一索引或Redis锁防止重复处理订单。证书管理推荐使用WechatPay Apache HttpClient简化证书处理:<dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-apache-httpclient</artifactId> <version>0.4.7</version></dependency>日志记录记录所有微信请求和回调,方便排查问题。四、完整调用示例(Spring Boot)@RestControllerpublic class PaymentController { @Autowired private WxPayService wxPayService; @GetMapping("/createOrder") public String createOrder(@RequestParam String orderId, @RequestParam int amount) { try { String codeUrl = wxPayService.createNativeOrder(orderId, amount); return "<img src=\"https://example.com/qr?data=" + codeUrl + "\">"; } catch (Exception e) { return "支付创建失败: " + e.getMessage(); } }}五、常见问题解决证书加载失败:检查证书路径和格式(必须为PEM格式)。签名错误:使用微信官方验签工具调试。回调未触发:检查notify_url是否外网可访问,且返回SUCCESS。通过以上步骤,可完成微信支付的核心对接,确保支付流程的可靠性和安全性。————————————————原文链接:https://blog.csdn.net/lilinhai548/article/details/146105883
-
文章目录#面向对象的介绍:一、设计对象并使用1.类和对象2.类的几个补充注意事项3.开发中类的设计二、封装1.封装的介绍2.封装的好处3.private关键字三、this关键字1.成员变量和局部变量2.举例代码详细解释:1. 类的成员变量定义2. 构造方法中的 `this` 关键字使用3. `set` 方法中的 `this` 关键字使用4. `get` 方法中的 `this` 关键字使用5. `displayInfo` 方法中的 `this` 关键字使用四、构造方法1.构造方法的概述2.构造方法的格式3.构造方法的作用4.构造方法的分类5.构造方法的注意事项五、标准JavaBean1.标准的JavaBean类六、对象内存图1.一个对象的内存图内存执行顺序解析(基于Java内存模型)**1. 类加载阶段(方法区)****2. 栈内存操作(main方法启动)****3. 堆内存分配(对象实例化)****4. 对象初始化流程****5. 变量关联与操作****6. 方法调用(方法区与栈协作)****内存操作完整流程总结****关键现象解释**2.多个对象的内存图**2.1、执行顺序与内存操作分步解析****1. 加载class文件(方法区)****2. 声明局部变量(栈内存)****3. 堆内存分配(对象空间开辟)****4. 默认初始化(堆内存)****5. 显示初始化(堆内存)****6. 构造方法初始化(堆内存)****7. 地址赋值(栈内存)****2.2、内存模型与对象独立性的关键验证****1. 对象独立性的体现****2. 内存操作流程图解****2.3、执行流程总结(分阶段)****2.4、常见问题解答****1. 为什么`System.out.println(s)` 输出地址?****2. 显示初始化和构造方法初始化有何区别?****3. 如何优化内存使用?**3.两个变量指向同一个对象内存图3.1、类加载阶段(方法区)3.2、栈内存操作(main方法栈帧)3.3、堆内存操作(对象关联)3.4、最终内存结构3.5、输出结果分析4.this的内存原理4.1、类加载阶段(方法区核心操作)4.2、对象实例化流程(堆栈协同)4.3、方法调用时的内存隔离(栈帧作用域)4.4、关键差异对比表4.5、技术扩展:`this`的底层实现5.基本数据类型和引用数据类型的区别5.1基本数据类型5.2引用数据类型七、补充知识:成员变量、局部变量区别#面向对象的介绍:面向:拿、找对象:能干活的东西面向对象编程:拿东西过来做对应的事情面向对象编程的例子:import java.util.Random;import java.util.Scanner;public class mian { public static void main(String[] args) { //面向对象,导入一个随机数 Random r = new Random(); int data = r.nextInt(10)+1; //面向对象,输入一个随机数 System.out.println(data); Scanner sc = new Scanner(System.in); // 面向对象,输出一个数 System.out.println("请输入一个数:"); int a = sc.nextInt(); System.out.println(a); }}为什么java要采取这种方法来编程呢?我们在程序之中要干某种事,需要某种工具来完成,这样更符合人类的思维习惯,编程更简单,更好理解。面向对象的重点学习对象是什么?学习获取已有对象并使用,学习如何自己设计对象并使用。——面向对象的语法一、设计对象并使用1.类和对象类(设计图):是对象共同特征的描述如何定义类:public class 类名{ 1.成员变量(代表属性,一般是名词) 2.成员方法(代表行为,一般是动词) 3.构造器(后面学习) 4.代码块(后面学习) 5.内部类(后面学习)}public class Phone{ //属性(成员变量) String brand; double price; public void call(){ } public void playGame(){ }}如何得到对象?如何得到类的对象:类名 对象名= new 类名();Phone p = new Phone();对象:是真实存在的具体东西拿到对象后能做什么?对象.成员变量;对象.成员方法(...)在JAVA中,必须先设计类,才获得对象public class phone { //属性 String name; double price; public void call(){ System.out.println("打电话"); } public void send(){ System.out.println("发短信"); }}//测试public class phoneTest { public static void main(String[] args) { //创建手机对象 phone p = new phone(); //给手机对象赋值 p.name = "小米"; p.price = 1999; //获取手机对象的属性值 System.out.println(p.name); System.out.println(p.price); //调用手机对象的方法 p.call(); p.send(); }}2.类的几个补充注意事项用来描述一类事物的类,专业叫做:Javabean类。在javabean类中,是不写main方法的。在以前,编写main方法的类,叫做测试类。我们可以在测试中创建javabean类的对象并进行赋值调用。public class 类名 { 1.成员变量(代表属性) 2.成员方法(代表行为)}public class Student { //属性(成员变量) String name; int age; //行为方法 public void study(){ System.out.println("好好学习,天天向上"); } public void doHomework(){ System.out.println("键盘敲烂,月薪过万"); }}类名首字母建议大写,需要见名知意,驼峰模式。一个java文件中可以定义多个class类,且只能一个类是public修饰的类名必须成为代码文件名。实际开发中建议还是一个文件定义一个class类。成员变量的完整定义格式是:修饰符 数据类型 变量名称=初始化值;一般无需指定初始化值,存在默认值。int age;//这里不写初始化值是因为,这里学生的年龄是一个群体的值,没有一个固定的初始化值。//如果给age赋值,比如是18岁,那就代表者所有的学生年龄都是18岁。//类的赋值不是在类里面赋值,而是在创建了对象之后再赋值,这时赋值的时这个特定的对象。Student stu = new Student();Stu.name="张三";Stu.height=187;对象的成员变量的默认值规则数据类型 明细 默认值基本类型 byte,short,int,long 0基本类型 float,double 0.0基本类型 boolean false引用类型 类、接口、数组、String null//编写女朋友类,创建女朋友类的对象,给女朋友的属性赋值并调用女朋友类中的方法。自己思考女朋友有哪些属性,有哪些行为?public class girlFriend { public static void main(String[] args) { //创建女朋友对象 girl g = new girl(); //给女朋友对象赋值 g.name = "小红"; g.age = 20; g.hobby = "唱歌"; //获取女朋友对象的属性值 System.out.println(g.name); System.out.println(g.age); System.out.println(g.hobby); //调用女朋友对象的方法 g.eat(); g.sleep(); }}//这是一个类public class girl { //成员变量(代表属性) String name; int age; String hobby; //成员方法(代表行为) public void eat(){ System.out.println("吃饭"); } public void sleep(){ System.out.println("睡觉"); }}3.开发中类的设计先把需求拿过来,先要看这个需求当中有几类事物。每个事物,每类事务都要定义为单独的类,这类事物的名词都可以定义为属性,这类事物的功能,一般是动词,可以定义为行为。二、封装1.封装的介绍封装是面向对象的三大特征:封装、继承、多态封装的作用:告诉我们,如何正确设计对象的属性和方法。/**需求:定义一个类描述人属性:姓名、年龄行为:吃饭、睡觉*/public class Person{ String name; int age; public void eat(){ System.out.println("吃饭"); } public void sleep(){ System.out.println("睡觉"); }}原则:对象代表什么,就得封装对应的数据,并提供数据对应的行为。public class Circle { double radius; public void draw(){ System.out.println("根据半径"+radius+"画圆"); }}//人画圆,我们通常人为行为主体是人,其实是圆//例如:人关门,这个门一定是门自己关的,人只是给了作用力,是门自己关上的。2.封装的好处对象代表什么,就得封装对应的数据,并提供数据对应的行为降低我们的学习成本,可以少学,少记,或者说压根不用学,不用记对象有哪些方法,有需要时去找就行3.private关键字是一个权限修饰符可以修饰成员(成员变量和成员方法)被private修饰的成员只能在本类中才能访问public class GirlFriend{ private String name; private int age; private String gender;}public class leiMing { private int age; //set(赋值) public void setAge(int a){ if(a<0||a>120){ System.out.println("你给的年龄有误"); return; } age = a; } //get(取值) public int getAge(){ return age; }}针对private修饰的成员变量,如果需要被其他类使用,提供相应的操作提供“setXxx(参数)”方法,用于给成员变量复制,方法用public修饰提供“getXxx()”方法,用于获取成员变量的值,方法用public修饰为什么要调用set和get呢?封装是面向对象编程的四大特性之一,它将数据(成员变量)和操作数据的方法绑定在一起,并隐藏对象的内部实现细节。通过将成员变量声明为 private,外部类无法直接访问和修改这些变量,只能通过类提供的 set 和 get 方法来间接操作。这样可以防止外部代码对数据进行非法或不恰当的修改,保证数据的安全性和完整性。三、this关键字1.成员变量和局部变量public class GirlFriend{ private int age;//成员变量:方法的外面,类的里面 public void method(){ int age = 10;//局部变量:方法的里面 System.out.println(age); }}成员变量和局部变量一致时,采用就近原则谁离我近,我就用谁public class GirlFriend{ private int age;//成员变量:方法的外面,类的里面 public void method(){ int age = 10;//局部变量:方法的里面 System.out.println(age); }}//在这里中,最后1个age距离 age=10最近,所以最后一个age用的是10的值//假如我想用第一个int ,我们可以在System.out.println(this.age)age前加入:this. 这里就可以打破就近原则,选择另一个变量在 Java 中,当局部变量(比如方法的参数)和类的成员变量重名时,就会产生命名冲突。在这种情况下,如果直接使用变量名,Java 默认会使用局部变量。而 this 关键字的一个重要作用就是用来引用当前对象的成员变量,从而区分局部变量和成员变量。2.举例下面通过一个简单的示例来详细讲解从引用成员变量方向 this 关键字的用法:class Employee { // 定义成员变量 private String name; private int age; // 构造方法,用于初始化员工信息 public Employee(String name, int age) { // 这里参数名和成员变量名相同,使用 this 引用成员变量 this.name = name; this.age = age; } // 设置员工姓名的方法 public void setName(String name) { // 使用 this 引用成员变量 this.name = name; } // 获取员工姓名的方法 public String getName() { return this.name; } // 设置员工年龄的方法 public void setAge(int age) { // 使用 this 引用成员变量 this.age = age; } // 获取员工年龄的方法 public int getAge() { return this.age; } // 显示员工信息的方法 public void displayInfo() { System.out.println("姓名: " + this.name + ", 年龄: " + this.age); }}public class ThisKeywordVariableExample { public static void main(String[] args) { // 创建一个 Employee 对象 Employee employee = new Employee("李四", 25); // 调用 displayInfo 方法显示员工信息 employee.displayInfo(); // 调用 setName 和 setAge 方法修改员工信息 employee.setName("王五"); employee.setAge(30); // 再次调用 displayInfo 方法显示修改后的员工信息 employee.displayInfo(); }}代码详细解释:1. 类的成员变量定义private String name;private int age;12这里定义了两个私有成员变量 name 和 age,用于存储员工的姓名和年龄。2. 构造方法中的 this 关键字使用public Employee(String name, int age) { this.name = name; this.age = age;}在构造方法中,参数名 name 和 age 与类的成员变量名相同。此时,this.name 表示当前对象的成员变量 name,而直接使用的 name 则是构造方法的参数(局部变量)。通过 this.name = name; 语句,将局部变量 name 的值赋给了当前对象的成员变量 name。同理,this.age = age; 也是将局部变量 age 的值赋给了成员变量 age。3. set 方法中的 this 关键字使用public void setName(String name) { this.name = name;}public void setAge(int age) { this.age = age;}在 setName 和 setAge 方法中,同样存在参数名和成员变量名相同的情况。使用 this 关键字来明确指定要操作的是当前对象的成员变量,避免了与局部变量的混淆。4. get 方法中的 this 关键字使用public String getName() { return this.name;}public int getAge() { return this.age;}在 get 方法中,使用 this.name 和 this.age 来返回当前对象的成员变量的值。虽然在这种情况下,不使用 this 关键字也可以正常返回成员变量的值,因为这里没有局部变量与成员变量重名的问题,但使用 this 可以使代码的意图更加清晰,表明是在访问当前对象的成员变量。5. displayInfo 方法中的 this 关键字使用public void displayInfo() { System.out.println("姓名: " + this.name + ", 年龄: " + this.age);}在 displayInfo 方法中,使用 this.name 和 this.age 来获取当前对象的成员变量的值,并将其输出。四、构造方法1.构造方法的概述构造方法也叫做构造器、构造函数2.构造方法的格式public class Student{ 修饰符 类名(参数){ 方法体; }}public class Student { private String name; private int age; //如果我们自己没有写构造方法 // 那么编译器会自动生成一个无参构造方法 public Student() { System.out.println("无参构造方法"); } public Student(String name, int age) { this.name = name; this.age = age; //有参构造方法 } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }}public class StudentTest { public static void main(String[] args) { //创建类的对象 //调用的空参构造 //Student s1 = new Student(); Student s = new Student(name:"张三", age:20); System.out.println(s.getName()); System.out.println(s.getAge()); }}特点:方法名与类名相同,大小写也要一致没有返回值类型,连void都没有没有具体的返回值(不能由return带回结果数据)执行时机:创建对象的时候由虚拟机调用,不能手动调用构造方法每创建一次对象,就会调用过一次构造方法3.构造方法的作用在创建对象的时候,由虚拟机自动调用构造方法,作用是给成员变量进行初始化的4.构造方法的分类public class Student{ private String name; private int age; public Student(){ ...//空参构造方法 } public Student (String name, int age){ ....//带全部参数构造方法 }}无参构造方法:初始化的对象时,成员变量的数据均采用默认值有参构造方法:在初始化对象的时候,同时可以为对象进行5.构造方法的注意事项构造方法的定义如果没有定义构造方法,系统将给出一个默认的无参数构造方法如果定义了构造方法,系统将不再提供默认的构造方法构造方法的重载带参构造方法,和无参构造方法,两者方法名相同,但是参数不同,这叫做构造方法的重载推荐的使用方式无论是否使用,都动手书写无参数构造方法,和带全部参数的构造方法五、标准JavaBean1.标准的JavaBean类类名需要见名知意成员变量使用private修饰提供至少两个构造方法无参构造方法带全部参数的构造方法成员方法提供每一个成员变量对应的setXxx()/getXxx()如果还有其他行为,也需要写上举例子:根据一个登录界面写一个JavaBean类public class User { //属性 private String username; private String password; private String email; private String gender; private int age; //构造方法 //无参构造 public User() { } //有参构造 public User(String username, String password, String email, String gender, int age) { this.username = username; this.password = password; this.email = email; this.gender = gender; this.age = age; } //方法 //set和get方法 public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }我们再写一个javabean中会遇到一个问题:这样写纯体力活啊!没事的没事的!我们有快捷键: 方法一: alt+insert 或 alt+insert+Fnalt+insert 第一个是构造函数,点击无选择生成的是空参 ,全选ok生成的是有参数的构造函数alt+insert 点击setter和geteer,全选生成的是set和get 方法二: 下载插件pdg,下载完成后点击空白处就会出现。然后点击Ptg To JavaBean六、对象内存图1.一个对象的内存图Student s = new Student();1加载class文件申明局部变量在堆中开辟一个空间默认初始化显示初始化构造方法初始化将堆中空间的地址值赋值给左边的局部变量举例:public class Student{ String name; int age; public void study(){ System.out.println("好好学习") }}public class TestStudent{ public static void main(String [] args){ Student s= new Student(); System.out.println(s); System.out.println(s.name+"...."+s.age); s.name = "阿强"; s.age = 23; System.out.println(s.name+"..."+s.age); s.study(); }}解析:内存执行顺序解析(基于Java内存模型)1. 类加载阶段(方法区)加载class文件JVM将Student.class和TestStudent.class加载到方法区,存储类结构信息(字段、方法签名、常量池等)。Student类包含字段name(String)、age(int)和方法study()。TestStudent类包含main()方法入口。2. 栈内存操作(main方法启动)声明局部变量执行main()时,在栈内存中创建main方法的栈帧,声明局部变量s(此时s未指向任何对象,值为null)。3. 堆内存分配(对象实例化)在堆中开辟空间执行new Student()时,在堆内存中为Student对象分配空间,内存大小由字段类型决定(String引用 + int值)。4. 对象初始化流程默认初始化对象字段赋默认值:name → null(引用类型默认值)age → 0(基本类型默认值)。显示初始化(本例中无)如果类中字段有显式赋值(如String name = "默认";),此时会执行。但示例代码未定义,此步骤跳过。构造方法初始化(本例中无)如果存在构造方法(如public Student() { age = 18; }),会通过构造器赋值。示例代码未定义构造方法,此步骤跳过。5. 变量关联与操作地址赋值给局部变量堆中对象地址赋值给栈帧中的s1变量,完成引用关联。执行Student s = new Student();后,s指向堆内存中的对象。对象字段修改后续代码通过s.name = "阿强";和s.age = 23;直接修改堆中对象的字段值,无需重新初始化。6. 方法调用(方法区与栈协作)执行s.study()从方法区加载study()的字节码指令。在栈中创建study()方法的栈帧,执行System.out.println(" 好好学习")(注:用户代码此处缺少分号,实际会编译报错)。内存操作完整流程总结步骤 操作内容 内存区域 示例代码体现1 加载类信息 方法区 Student和TestStudent类加载2 声明局部变量s 栈内存 Student s;3 堆中分配对象空间 堆内存 new Student()4 字段默认初始化(null/0) 堆内存 s.name 和s.age 初始值5 显式/构造初始化(无) - 代码未定义相关逻辑6 对象地址赋值给s 栈内存 s = new Student();7 修改字段值 堆内存 s.name = "阿强";等操作关键现象解释System.out.println(s) 输出哈希值因打印对象时默认调用toString(),而Student未重写该方法,输出格式为类名@哈希值。字段值修改的可见性直接通过引用s修改堆中对象字段,所有指向该对象的引用均会看到更新后的值。编译隐患study()方法中System.out.println(" 好好学习")缺少分号,实际运行前会因语法错误中断。2.多个对象的内存图举例:public class Student{ String name; int age; public void study(){ System.out.println("好好学习"); }}public class TestStudent{ public static void main(String [] args){ Student s= new Student(); System.out.println(s); s.name = "阿强"; s.age = 23; System.out.println(s.name+"..."+s.age); s.study(); Student s2= new Student(); System.out.println(s2); s2.name = "阿珍"; s2.age = 24; System.out.println(s2.name+"..."+s2.age); s2.study(); }}第二次在创建对象时。class文件不需要再加载一次解析:2.1、执行顺序与内存操作分步解析1. 加载class文件(方法区)触发条件:首次使用Student类时。操作内容:将Student.class 加载到方法区,存储类结构(字段name、age和方法study()的定义)。将TestStudent.class 加载到方法区,存储main()方法入口。2. 声明局部变量(栈内存)操作内容:执行main()方法时,在栈内存创建main方法的栈帧。声明局部变量s和s2(初始值均为null)。3. 堆内存分配(对象空间开辟)操作内容:new Student()触发堆内存分配,根据类结构计算对象大小(String引用 + int值)。示例:s = new Student() → 堆地址0x001。s2 = new Student() → 堆地址0x002(独立空间)。4. 默认初始化(堆内存)操作内容:对象字段赋默认值:name → null(引用类型默认值)。age → 0(基本类型默认值)。示例:s的初始状态:name=null, age=0。s2的初始状态:name=null, age=0。5. 显示初始化(堆内存)触发条件:类中显式赋值的字段(如String name = "默认")。当前代码:Student类未定义显式赋值字段,跳过此步骤。6. 构造方法初始化(堆内存)触发条件:存在自定义构造方法(如public Student() { ... })。当前代码:Student类未定义构造方法,使用默认无参构造器,跳过此步骤。7. 地址赋值(栈内存)操作内容:将堆内存地址赋值给栈中的局部变量。示例:s = 0x001(指向第一个对象)。s2 = 0x002(指向第二个对象)。2.2、内存模型与对象独立性的关键验证1. 对象独立性的体现对象 堆地址 字段修改后的值s 0x001 name="阿强", age=23s2 0x002 name="阿珍", age=24验证逻辑:s和s2指向不同堆地址,修改其中一个对象的字段不会影响另一个对象。System.out.println(s == s2) → 输出false。2. 内存操作流程图解2.3、执行流程总结(分阶段)阶段 操作内容 内存区域类加载 加载Student和TestStudent类信息 方法区栈帧创建 声明s和s2(初始null) 栈内存堆内存分配 为s和s2分配独立空间 堆内存对象初始化 默认初始化 → 显式赋值(用户代码修改) 堆内存方法调用 study()从方法区加载逻辑到栈执行 栈内存2.4、常见问题解答1. 为什么System.out.println(s) 输出地址?原因:未重写toString()方法,默认调用Object.toString() ,格式为类名@哈希值。2. 显示初始化和构造方法初始化有何区别?显示初始化:直接在类中赋值字段(如String name = "张三"),编译时自动插入到构造方法中。构造方法初始化:通过自定义构造器赋值(优先级高于显示初始化)。3. 如何优化内存使用?复用对象:避免频繁new对象(尤其循环中)。垃圾回收:main()结束后,s和s2成为垃圾对象,由GC自动回收。附:修正后的代码输出示例Student@1b6d3586 阿强...23 好好学习 Student@4554617c 阿珍...24 好好学习 3.两个变量指向同一个对象内存图举例:public class Student{ String name; int age; public void study(){ System.out.println("好好学习"); }}public class TestStudent{ public static void main(String [] args){ Student s= new Student(); s.name = "阿强"; Student s2= s; s2.name = "阿珍"; System.out.println(s.name+"..."+s2.name); }}3.1、类加载阶段(方法区)加载TestStudent.class当JVM启动时,首先将TestStudent.class 加载到方法区,存储类结构信息(成员方法、字段描述等)加载Student.class执行new Student()时触发类加载机制,将Student.class 加载到方法区,包含name、age字段和study()方法元数据3.2、栈内存操作(main方法栈帧)声明局部变量在main方法栈帧中创建引用变量s(地址未初始化)和s2(此时两者均为null)对象创建指令new Student()操作码触发堆内存分配,此时:在堆中生成对象内存空间(包含对象头 + String name + int age)默认初始化:name=null,age=0(基本类型和引用类型的零值初始化)显式初始化:由于Student类没有直接赋值的字段(如String name = "默认名"),此阶段跳过构造方法执行若存在构造方法(本案例未定义),会通过invokespecial指令调用<init>方法完成初始化3.3、堆内存操作(对象关联)地址赋值将堆中Student对象地址赋值给栈帧中的s变量(完成s = new Student())引用传递s2 = s操作使s2指向堆中同一个对象(此时两个引用共享对象数据)字段修改通过s2.name = "阿珍"修改堆内存对象数据,此时s.name 同步变化(引用指向同一实体)3.4、最终内存结构内存区域 存储内容方法区 TestStudent类字节码、Student类元数据(包含study()方法代码)堆内存 Student对象实例(name=“阿珍”, age=0)栈内存 main方法栈帧:s=0x100(指向堆对象), s2=0x100(与s同地址)3.5、输出结果分析System.out.println(s.name+"..."+s2.name)→ 输出阿珍...阿珍(s与s2引用同一对象,堆内数据修改对所有引用可见)关键理解点:引用类型变量的赋值操作传递的是对象地址值,而非创建新对象。这种特性是Java对象共享机制的核心体现。4.this的内存原理public class Student{ private int age; public void method(){ int age=10; System.out.println(age);//10 System.out.println(this.age);//成员变量的值 0 }}this的作用:区分局部变量和成员变量this的本质:所在方法调用者的地址值public class Student{ private int age; public void method(){ int age=10; System.out.println(age);//10 System.out.println(this.age);//成员变量的值 0 }}public class StudentTest{ public static void main (String[] args){ Student s = new Student(); s.method(); }}4.1、类加载阶段(方法区核心操作)加载StudentTest.classJVM启动时优先加载含main()的类到方法区存储类元数据:静态变量、方法表(含main()入口地址)触发Student.class 加载当执行new Student()时触发类加载方法区新增:字段描述表(private int age的访问权限和偏移量)method()的字节码指令集合隐式默认构造器<init>方法(因无自定义构造方法)4.2、对象实例化流程(堆栈协同)步骤 内存区域 具体行为 代码对应3 栈内存 在main方法栈帧声明局部变量s(初始值null) Student s;4 堆内存 分配对象空间:对象头(12字节)+ int age(4字节)= 16字节 new Student()5 堆内存 默认初始化:age=0(基本类型零值填充) 隐式执行6 堆内存 构造方法初始化:执行空参数的<init>方法(无实际操作) 隐式调用7 栈内存 将堆地址(如0x7a3f)赋值给s变量 s = new...4.3、方法调用时的内存隔离(栈帧作用域)执行s.method() 时发生:新建栈帧:在栈顶创建method()1的独立空间,包含:隐式参数this(指向堆地址0x7a3f)局部变量age=10(存储于栈帧变量表)变量访问规则:输出语句 内存访问路径 结果System.out.println(age) 访问栈帧局部变量表 10System.out.println(this.age) 通过this指针访问堆内存字段 04.4、关键差异对比表特征 成员变量this.age 局部变量age存储位置 堆内存对象内部 栈帧局部变量表生命周期 与对象共存亡 随方法栈帧销毁而消失初始化值 默认零值(int=0) 必须显式赋值访问方式 需通过对象引用 直接访问4.5、技术扩展:this的底层实现当调用method()时:字节码层面:java复制aload_0 // 将this引用压入操作数栈(对应堆地址0x7a3f)getfield #2 // 根据字段偏移量读取堆中age值(#2为字段符号引用)内存隔离机制:局部变量age会遮蔽同名的成员变量,必须通过this.显式穿透访问堆数据5.基本数据类型和引用数据类型的区别5.1基本数据类型public class Test{ public static void main (String [] args){ int a = 10; }}基本数据类型:在变量当中存储的是真实的数据值从内存角度:数据值是存储再自己的空间中特点:赋值给其他变量,也是赋值的真实的值。5.2引用数据类型public class TestStudent{ public static void main(String[] args){ Student s=new Student; }}引用数据类型:堆中存储的数据类型,也就是new出来的,变量中存储的是地址值。引用:就是使用其他空间中数据的意思。从内存的角度:数据值是存储在其他空间中,自己空间中存储的是地址值特点:赋值给其他变量,赋的地址值。七、补充知识:成员变量、局部变量区别成员变量:类中方法外的变量局部变量:方法中的变量 区别:区别 成员变量 局部变量类中位置不同 类中,方法外 方法内、方法申明上初始化值不同 有默认初始化值 没有,使用前需要完成赋值内存位置不同 堆内存 栈内存生命周期不同 随着对象的创建而存在,随着对象的消失而消失 随着方法的调用而存在,随着方法的运行结束而消失作用域 整个类中有效 当前方法中有效————————————————原文链接:https://blog.csdn.net/2401_87533975/article/details/145992557