📚 Versión final PRO
Esta versión une todo el contenido extenso con una navegación arreglada y una capa práctica para estudiar mejor: explicaciones como profesor, casos reales, mini ejercicios tipo examen y resúmenes ultra rápidos.
🧪 En práctica — cómo pensarlo
var, si no cambia val; si puede faltar, el tipo debe admitir null.CASO REAL Ejemplo de uso
Valida un login: recorta texto, comprueba vacíos y devuelve un mensaje. Ahí practicas funciones, null safety e interpolación de strings.
FLUJO Pasos mentales
- Leer el dato del usuario
- Quitar espacios con
trim() - Comprobar si está vacío
- Devolver el resultado desde una función
🎯 Mini ejercicio tipo examen
Crea una clase Producto con nombre y precio. Añade una función que devuelva "Producto: X - 9.99€" y otra que aplique un 15% de descuento.
⚡ Resumen ultra rápido
val= no cambiavar= sí cambia?= admite null?.evita el error por null?:da un valor alternativo
📌 ¿Qué es Kotlin?
Kotlin es el lenguaje principal de Android. Funciona como un lenguaje transpilado: tú escribes Kotlin → se convierte a Java → la JVM lo ejecuta. Esto significa que todo lo que funciona en Java, funciona en Kotlin, pero con mucha menos ceremonia.
🔤 Variables
Dos palabras reservadas: var (mutable) y val (inmutable/constante).
// Kotlin infiere el tipo automáticamente
var nombre: String = "Borja" // mutable
val PI: Double = 3.14159 // inmutable (como final en Java)
// Null Safety — tipos con ? admiten nulos
var correo: String? = null // puede ser null
var nombre2: String = "Ana" // NUNCA puede ser null
// Operador Elvis: si es null, usa el valor alternativo
val displayCorreo = correo ?: "Sin correo"kotlin
🔧 Funciones
// En Java: public static void main(String[] args)
// En Kotlin: simplemente fun main()
fun main() {
println("Hola mundo") // equivale a System.out.println
}
// Función con parámetros y retorno
fun sumar(a: Int, b: Int): Int {
return a + b
}
// Parámetros con valor por defecto
fun saludar(nombre: String = "mundo") {
println("Hola $nombre") // interpolación de strings con $
}
// Llamada con parámetros nominales (muy usado en Flutter/Android)
saludar(nombre = "Alumno")kotlin
🏗️ Clases y Constructores
Kotlin distingue entre constructor primario (parámetros obligatorios) y constructores secundarios (opcionales, se basan en el primario).
// Constructor primario → en la cabecera de la clase
class Alumno(var id: Int = 0, var nombre: String, var apellido: String) {
// Atributo opcional (puede ser null)
var correo: String? = null
// Constructor secundario — SIEMPRE llama al primario con : this(...)
constructor(id: Int, nombre: String, apellido: String, correo: String)
: this(id, nombre, apellido) {
this.correo = correo // lo extra se asigna aquí
}
// Método (aquí se llama "función")
fun mostrarDatos() {
println("ID: $id | Nombre: $nombre $apellido | Correo: ${correo ?: "Sin correo"}")
}
}
// Crear objetos — sin "new"
val alumno1 = Alumno(nombre = "Borja", apellido = "Martín")
val alumno2 = Alumno(1, "Ana", "López", "[email protected]")
alumno1.mostrarDatos()
// Los getters/setters son implícitos — alumno1.nombre funciona directamentekotlin
: this(...).🛡️ Null Safety — Muy importante
var correo: String? = null
// ❌ Esto da error de compilación — puede ser null
println(correo.length)
// ✅ Operador safe-call — solo ejecuta si NO es null
println(correo?.length)
// ✅ Operador Elvis — valor alternativo si es null
println(correo ?: "Sin correo")
// ✅ late init — inicialización diferida (para binding en Android)
private lateinit var binding: ActivityMainBindingkotlin
🧪 En práctica — cómo pensarlo
onCreate(): crear binding, pintar el XML y preparar listeners. Si una vista “no existe”, casi siempre el problema está en ese arranque.CASO REAL Ejemplo de uso
Pantalla con un EditText, un botón y un TextView. Al pulsar el botón se lee el texto y se muestra “Hola, nombre”.
FLUJO Pasos mentales
- Inflar el binding
- Llamar a
setContentView(binding.root) - Registrar listeners
- Actualizar la interfaz
🎯 Mini ejercicio tipo examen
Haz una Activity que tenga nombre y edad. Al pulsar un botón, debe mostrar “Ana tiene 20 años”.
⚡ Resumen ultra rápido
onCreate()= arranque- Binding conecta XML y Kotlin
- Sin
setContentViewno se ve la pantalla - Los listeners van después de inicializar binding
🗂️ Estructura del Proyecto
- src/main/java → Código Kotlin (lógica)
- res/layout → Ficheros XML (parte gráfica)
- res/drawable → Imágenes locales
- res/values → Colores, strings, estilos
- AndroidManifest.xml → Registro de activities y permisos
- build.gradle → Dependencias y configuración del proyecto
R.tipo.nombre. Ejemplo: R.layout.activity_main, R.drawable.login♻️ Ciclo de Vida de una Activity
onCreate()
Primer método. Aquí se inicializa el binding y se asocia el XML con la clase Kotlin. Es donde debes poner tu inicialización.
onStart()
La pantalla empieza a ser visible.
onResume()
La app está en primer plano e interactiva.
onPause()
Llega una notificación, otra app pasa a primer plano.
onStop()
La app deja de ser visible (~2-3 segundos en segundo plano).
onDestroy()
Al girar el móvil o cambiar idioma → se destruye y vuelve a crear desde cero.
🔗 View Binding — La forma de conectar XML con Kotlin
View Binding genera automáticamente una clase Kotlin por cada XML, con todos los elementos como propiedades directas.
Paso 1 — Activar en build.gradle (app)
android {
...
buildFeatures {
viewBinding = true // ← añadir esto
}
}
// Después: Sync Now (imprescindible)gradle
Paso 2 — Inicializar en la Activity
class MainActivity : AppCompatActivity() {
// lateinit porque no la podemos inicializar antes de onCreate
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Inicializar el binding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Ahora puedes acceder a todos los elementos del XML directamente
binding.botonEnviar.setOnClickListener {
val texto = binding.editTextoNombre.text.toString()
binding.textResultado.text = "Hola, $texto"
}
}
}kotlin
activity_main.xml → ActivityMainBinding. El ID del elemento en XML → propiedad del binding (@+id/botonEnviar → binding.botonEnviar).🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Formulario de login: logo arriba, dos campos y botón centrados. Con guideline puedes fijar el logo en el 40% superior.
FLUJO Pasos mentales
- Elegir el contenedor correcto
- Dar restricciones mínimas a cada vista
- Probar orientación vertical y horizontal
- Añadir listeners según el tipo de vista
🎯 Mini ejercicio tipo examen
Diseña una pantalla con imagen, usuario, contraseña y botón. Luego explica por qué usarías LinearLayout o ConstraintLayout.
⚡ Resumen ultra rápido
- LinearLayout = simple
- ConstraintLayout = flexible
- Cada vista necesita restricciones válidas
- No todos los controles usan el mismo listener
📐 LinearLayout vs ConstraintLayout
- LinearLayout → Elementos de arriba a abajo o izquierda a derecha. Simple y rápido. Usa pesos (
layout_weight) para distribuir espacio. - ConstraintLayout → Posicionamiento relativo mediante restricciones. Más flexible pero más configuración. Cada elemento necesita mínimo 2 restricciones (una por eje).
🔒 ConstraintLayout — Restricciones
<!-- Ejemplo: imagen centrada en el 40% superior -->
<ImageView
android:id="@+id/imagenLogin"
android:layout_width="250dp"
android:layout_height="250dp"
android:scaleType="fitXY"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/guia" />
<!-- Guideline: línea invisible al 40% de la pantalla -->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guia"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.4" />xml
👆 Eventos — Escuchadores
Cada tipo de vista tiene su propio listener. No todos usan setOnClickListener.
// Botón — click normal
binding.btnLogin.setOnClickListener {
// se ejecuta al pulsar
}
// EditText — cambio de texto
binding.editNombre.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
// s contiene el texto actual
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
// Switch — cambio de estado
binding.switchRecuerdame.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) { /* activado */ } else { /* desactivado */ }
}
// Spinner — selección de item
binding.spinnerRol.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
val item = parent.getItemAtPosition(pos).toString()
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}kotlin
🖼️ Imágenes — Local vs CDN
- Local: Poner el fichero en res/drawable. En XML:
android:src="@drawable/login". Problema: aumenta el peso del APK. - CDN: Subir imagen a un servidor y cargar la URL con Glide/Picasso. La app no pesa, carga al vuelo. Es lo que se usa en producción.
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Lista de productos con imagen, nombre y botón “Comprar”. El adaptador rellena cada fila y el botón lanza un Toast.
FLUJO Pasos mentales
- Modelo de datos
- XML contenedor
- XML de la fila
- Adapter + ViewHolder
- Asignar adapter y layoutManager
🎯 Mini ejercicio tipo examen
Haz una lista de alumnos con nombre y nota. Al tocar un botón de la fila, muestra “Alumno aprobado/suspenso”.
⚡ Resumen ultra rápido
- Fila =
wrap_content - Adapter conecta datos y vistas
- ViewHolder recicla
layoutManagerdecide la forma de la lista
🗺️ Los 5 pasos del RecyclerView
- Crear el modelo de datos (clase Kotlin con los atributos del elemento).
- Definir el RecyclerView en el XML de la Activity.
- Crear el XML de la fila (item_recycler_producto.xml) — el template que se repetirá.
- Crear el Adaptador (clase que conecta datos con vistas).
- Instanciar y configurar el RecyclerView en el código Kotlin.
1️⃣ Modelo de datos
// model/Producto.kt
class Producto(var title: String, var price: Double,
var description: String, var stock: Int, var url: String)kotlin
2️⃣ XML de la Activity (el contenedor)
<LinearLayout ...>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerProductos"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>xml
3️⃣ XML de la fila (item_recycler_producto.xml)
<!-- ⚠️ IMPORTANTE: layout_height debe ser "wrap_content", NO "match_parent"
Si pones match_parent, cada fila ocupa una pantalla entera -->
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp">
<ImageView
android:id="@+id/imagenRecycler"
android:layout_width="200dp"
android:layout_height="200dp" ... />
<TextView
android:id="@+id/nombreRecycler"
tools:text="Ejemplo" <!-- tools: solo visible en editor, no en app -->
... />
<Button
android:id="@+id/btnRecycler"
android:text="Comprar" ... />
</ConstraintLayout>xml
4️⃣ El Adaptador — La pieza clave
class ProductoAdapter(
private val context: Context,
private val lista: List<Producto>
) : RecyclerView.Adapter<ProductoAdapter.MyHolder>() {
// ViewHolder — patrón que recicla vistas para mejor rendimiento
inner class MyHolder(binding: ItemRecyclerProductoBinding)
: RecyclerView.ViewHolder(binding.root) {
val imagen = binding.imagenRecycler
val nombre = binding.nombreRecycler
val boton = binding.btnRecycler
}
// Infla el XML de la fila y devuelve el Holder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder {
val binding = ItemRecyclerProductoBinding.inflate(
LayoutInflater.from(context), parent, false
)
return MyHolder(binding)
}
// Rellena los datos de cada fila según su posición
override fun onBindViewHolder(holder: MyHolder, position: Int) {
val producto = lista[position]
holder.nombre.text = producto.title
holder.boton.setOnClickListener {
Toast.makeText(context, "Comprando ${producto.title}", Toast.LENGTH_SHORT).show()
}
}
// Número de elementos en la lista
override fun getItemCount() = lista.size
}kotlin
5️⃣ Configurar en la Activity
val listaProductos = mutableListOf(
Producto("Zapatillas", 99.99, "Cómodas", 10, "https://..."),
Producto("Camiseta", 29.99, "Algodón", 50, "https://...")
)
val adaptador = ProductoAdapter(this, listaProductos)
binding.recyclerProductos.adapter = adaptador
// LinearLayoutManager = lista vertical
// GridLayoutManager(this, 2) = cuadrícula de 2 columnas
binding.recyclerProductos.layoutManager = LinearLayoutManager(this)kotlin
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Pedir usuarios a una API, recorrer el JSON, crear objetos Usuario y añadirlos al RecyclerView.
FLUJO Pasos mentales
- Comprobar permiso de internet
- Elegir si la respuesta es objeto o array
- Parsear el JSON
- Actualizar el adaptador
🎯 Mini ejercicio tipo examen
Lee una API que devuelva productos y muestra solo nombre y precio en una lista.
⚡ Resumen ultra rápido
- Nunca hagas red en main thread
- Volley gestiona el hilo
- JSON se parsea campo a campo
- El adapter se actualiza al recibir datos
🧵 Hilos y Corrutinas
Android tiene un hilo principal (main thread) que hace DOS cosas: renderizar la pantalla y escuchar eventos. Si haces una petición de red en este hilo → la pantalla se congela.
Volley gestiona esto automáticamente — tú haces la petición, Volley crea el hilo, cuando llegan los datos los pasa al hilo principal para pintar.
📦 Configurar Volley
1. Añadir dependencia en build.gradle
dependencies {
implementation("com.android.volley:volley:1.2.1")
}gradle
2. Permiso de Internet en AndroidManifest.xml
<manifest>
<!-- Antes de <application> -->
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>xml
🌐 Petición JSON con Volley
val url = "https://dummyjson.com/users"
// ¿Es JSONObject o JSONArray? Mira la URL:
// Empieza con { → JSONObjectRequest
// Empieza con [ → JSONArrayRequest
val peticion = JsonObjectRequest(
Request.Method.GET, url, null,
{ response ->
// Respuesta OK — ya estamos en el hilo principal
val usuarios = response.getJSONArray("users")
for (i in 0 until usuarios.length()) {
val obj = usuarios.getJSONObject(i)
val nombre = obj.getString("firstName")
val apellido = obj.getString("lastName")
// Añadir al adaptador...
}
},
{ error ->
// Error de red
Log.e("VOLLEY", error.toString())
}
)
// Ejecutar la petición mediante la cola de Volley
Volley.newRequestQueue(this).add(peticion)kotlin
{ es Object, si empieza con [ es Array.🔄 Adaptador con lista dinámica (sin pasar lista al constructor)
Cuando los datos llegan de una API, no puedes pasar la lista al constructor porque aún no tienes los datos. La lista se rellena poco a poco.
class UsuarioAdapter(private val context: Context)
: RecyclerView.Adapter<UsuarioAdapter.MyHolder>() {
// Lista interna vacía — se rellena con addUser()
private lateinit var lista: ArrayList<Usuario>
init { lista = ArrayList() }
// Método para añadir un usuario desde la Activity
fun addUser(user: Usuario) {
lista.add(user)
// Notifica SOLO la posición insertada (más eficiente que notifyDataSetChanged)
notifyItemInserted(lista.size - 1)
}
override fun getItemCount() = lista.size
// ... onCreateViewHolder y onBindViewHolder igual que antes
}
// En la Activity, dentro del listener de Volley:
val user = Usuario(nombre, apellido)
adapter.addUser(user) // el adaptador se actualiza solokotlin
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Opciones en toolbar para “Nuevo”, “Editar” y “Salir”; diálogo para confirmar borrado o pedir un nombre.
FLUJO Pasos mentales
- Crear XML del menú
- Inflarlo en la Activity
- Gestionar la pulsación
- Mostrar diálogo si hace falta
🎯 Mini ejercicio tipo examen
Crea un menú con “Añadir” y “Borrar todo”. “Añadir” abre un diálogo con EditText; “Borrar todo” pide confirmación.
⚡ Resumen ultra rápido
onCreateOptionsMenupinta el menúonOptionsItemSelectedresponde- AlertDialog sirve para confirmar o pedir datos
🍔 Menú en Toolbar
1. Crear el XML del menú (res/menu/menu_principal.xml)
<menu>
<item
android:id="@+id/action_settings"
android:title="Ajustes"
app:showAsAction="never" />
<item
android:id="@+id/action_logout"
android:title="Cerrar sesión"
app:showAsAction="never" />
</menu>xml
2. Inflar y gestionar en la Activity
// Inflar el menú
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_principal, menu)
return true
}
// Gestionar qué hace cada opción
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_settings -> { /* ir a ajustes */; true }
R.id.action_logout -> { /* cerrar sesión */; true }
else -> super.onOptionsItemSelected(item)
}
}kotlin
💬 AlertDialog
AlertDialog.Builder(this)
.setTitle("Confirmación")
.setMessage("¿Seguro que quieres borrar esto?")
.setPositiveButton("Sí") { _, _ ->
// acción al confirmar
}
.setNegativeButton("No", null)
.show()kotlin
📋 Menú de opciones (OptionsMenu) — pasos completos
// 1. Crear /res/menu/menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_nuevo"
android:title="Nuevo"
app:showAsAction="ifRoom|withText"/>
<item android:id="@+id/action_editar"
android:title="Editar"
app:showAsAction="never"/>
<item android:id="@+id/action_salir"
android:title="Salir"
app:showAsAction="never"/>
</menu>xml
// 2. Inflar el menú en la Activity
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
// 3. Gestionar pulsaciones
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_nuevo -> { mostrarDialogo(); true }
R.id.action_editar -> { editarElemento(); true }
R.id.action_salir -> { finish(); true }
else -> super.onOptionsItemSelected(item)
}
}kotlin
💬 AlertDialog — cuadro de diálogo completo
private fun mostrarDialogo() {
// Dialog con input (EditText dentro)
val editText = EditText(this)
editText.hint = "Introduce el nombre"
AlertDialog.Builder(this)
.setTitle("Nuevo elemento")
.setMessage("¿Cómo se llama?")
.setView(editText)
.setPositiveButton("Aceptar") { _, _ ->
val texto = editText.text.toString().trim()
if (texto.isNotEmpty()) {
Toast.makeText(this, "Añadido: $texto", Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton("Cancelar", null) // null = solo cierra
.show()
}
// Dialog de confirmación (sin input)
private fun confirmarBorrado() {
AlertDialog.Builder(this)
.setTitle("¿Borrar?")
.setMessage("Esta acción no se puede deshacer")
.setPositiveButton("Borrar") { _, _ -> borrarElemento() }
.setNegativeButton("Cancelar", null)
.show()
}kotlin
📌 ContextMenu — menú al mantener pulsado
// 1. Registrar la vista en onCreate
registerForContextMenu(binding.listaElementos)
// 2. Crear el menú contextual
override fun onCreateContextMenu(menu: ContextMenu?, v: View?,
menuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(menu, v, menuInfo)
menuInflater.inflate(R.menu.context_menu, menu)
menu?.setHeaderTitle("Opciones")
}
// 3. Gestionar la selección
override fun onContextItemSelected(item: MenuItem): Boolean {
val info = item.menuInfo as AdapterView.AdapterContextMenuInfo
val posicion = info.position // posición del item pulsado en la lista
return when (item.itemId) {
R.id.ctx_editar -> { editarEnPosicion(posicion); true }
R.id.ctx_borrar -> { borrarEnPosicion(posicion); true }
else -> super.onContextItemSelected(item)
}
}kotlin
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Primera pantalla con botón “Siguiente”; al pulsar navega a un segundo fragment y le pasa un nombre por Bundle.
FLUJO Pasos mentales
- Crear el fragment
- Inflar su binding en
onCreateView - Poner lógica en
onViewCreated - Navegar con
findNavController()
🎯 Mini ejercicio tipo examen
Haz dos fragments: en el primero escribes un nombre y en el segundo lo muestras.
⚡ Resumen ultra rápido
- Fragment ≠ Activity
onCreateViewcrea la vistaonViewCreatedmete la lógica- Navigation Graph define el camino
🧩 ¿Qué es un Fragment?
Un Fragment es como una "sub-pantalla" reutilizable que vive dentro de una Activity. En vez de tener 10 Activities, tienes 1 Activity con 10 Fragments que van apareciendo y desapareciendo.
Tipos:
- Estático: Siempre visible en el mismo sitio. Se define con la etiqueta
<fragment>directamente en el XML. - Dinámico: Aparece y desaparece según la navegación. Es el más común hoy en día.
🚀 Proyecto con Fragments — Basic Views Activity
En vez de "Empty Views Activity", al crear el proyecto elige "Basic Views Activity". Ya viene con:
- View Binding activado
- Toolbar configurado
- Menú creado
- Navigation Graph configurado
- NavHostFragment en el XML
🗺️ Navigation Graph
El Navigation Graph (res/navigation/nav_graph.xml) define qué fragments existen y cómo se navega entre ellos.
<!-- nav_graph.xml -->
<navigation app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.example.tienda.FirstFragment">
<!-- acción para navegar al segundo -->
<action
android:id="@+id/action_first_to_second"
app:destination="@id/secondFragment" />
</fragment>
<fragment
android:id="@+id/secondFragment"
android:name="com.example.tienda.SecondFragment" />
</navigation>xml
Navegar entre fragments desde código:
// Dentro de un Fragment
binding.btnIrSegundo.setOnClickListener {
findNavController().navigate(R.id.action_first_to_second)
}
// Pasar datos entre fragments (Safe Args o Bundle)
val bundle = Bundle()
bundle.putString("nombre", "Borja")
findNavController().navigate(R.id.action_first_to_second, bundle)
// En el fragment destino, recoger el dato:
val nombre = arguments?.getString("nombre")kotlin
📋 Fragment — Estructura básica
class FirstFragment : Fragment() {
private lateinit var binding: FragmentFirstBinding
// En Fragment usamos onCreateView, NO onCreate
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
binding = FragmentFirstBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Aquí pones la lógica (listeners, datos, etc.)
binding.btnSiguiente.setOnClickListener {
findNavController().navigate(R.id.action_first_to_second)
}
}
}kotlin
onCreateView + onViewCreated, NO onCreate como en Activity.🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Registro con email y contraseña; si sale bien, guardas datos de perfil en Realtime Database con el uid como clave.
FLUJO Pasos mentales
- Inicializar Firebase
- Registrar o loguear usuario
- Obtener
uid - Guardar o leer nodos
🎯 Mini ejercicio tipo examen
Haz un alta de usuario y guarda nombre, email y edad en un nodo usuarios/uid.
⚡ Resumen ultra rápido
- Auth = sesión
- Realtime DB = árbol JSON
push()crea id automático- Los listeners actualizan en tiempo real
🔥 ¿Por qué Firebase?
Firebase es el backend "listo para usar" de Google. Te ahorra crear tu propio servidor. Sus dos servicios principales que veremos:
- Authentication: Login y registro de usuarios
- Realtime Database: Base de datos NoSQL en tiempo real
🔐 Authentication — Login y Registro
val auth = FirebaseAuth.getInstance()
// Registro de usuario
auth.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
// Registro OK — it.user contiene el usuario creado
val uid = it.user?.uid // ID único del usuario
}
.addOnFailureListener { e ->
// Error: email ya existe, contraseña débil, etc.
Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show()
}
// Login
auth.signInWithEmailAndPassword(email, password)
.addOnSuccessListener {
// Login OK
startActivity(Intent(this, MainActivity::class.java))
}
.addOnFailureListener { /* credenciales incorrectas */ }
// Cerrar sesión
auth.signOut()
// Comprobar si hay sesión activa
if (auth.currentUser != null) { /* hay sesión */ }kotlin
🗄️ Realtime Database — Estructura de Nodos
No hay tablas. Los datos se organizan en nodos (árbol JSON).
- Nodo rama: sus hijos son otros nodos.
- Nodo hoja: sus hijos son valores (String, Int, Boolean, Object).
// Ejemplo de estructura en Firebase:
// mi-app/
// ├── productos/
// │ ├── id1/
// │ │ ├── nombre: "Zapatillas"
// │ │ └── precio: 99.99
// │ └── id2/ ...
// └── usuarios/ ...json
Escribir datos
val db = FirebaseDatabase.getInstance().reference
// Crear/actualizar nodo
db.child("productos").child("id1").setValue(producto)
.addOnSuccessListener { /* guardado OK */ }
// Firebase genera un ID único automáticamente con push()
db.child("productos").push().setValue(producto)kotlin
Leer datos (en tiempo real)
db.child("productos").addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
// Se ejecuta al cargar y CADA VEZ que haya un cambio
for (child in snapshot.children) {
val producto = child.getValue(Producto::class.java)
// añadir al adaptador...
}
}
override fun onCancelled(error: DatabaseError) {}
})kotlin
addValueEventListener es como un listener permanente — cuando alguien en otro país modifica un dato, tu app lo recibe automáticamente sin refrescar.⚠️ Configurar permisos de la DB
Al crear la base de datos, elige Modo Prueba. Esto permite leer y escribir durante 30 días sin autenticación. Las reglas en JSON:
{
"rules": {
".read": true, // cualquiera puede leer
".write": true // cualquiera puede escribir
}
}
// En producción esto debe ser más restrictivo (solo usuarios autenticados)json
🆚 Realtime DB vs Firestore
- Realtime Database → Datos pequeños-medianos. Estructura de nodos. Más sencillo. ← Lo que usamos en clase
- Firestore → Grandes volúmenes de datos. Colecciones + documentos (similar a MongoDB).
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Pantalla simple con Column, texto y botón; al tocar el botón aumenta un contador con setState().
FLUJO Pasos mentales
- Crear widget
- Construir UI en
build() - Guardar estado
- Actualizar con
setState()
🎯 Mini ejercicio tipo examen
Haz un contador con botones +1 y -1 y muestra “positivo / cero / negativo”.
⚡ Resumen ultra rápido
- Todo es widget
build()dibujasetState()refresca- Column/Row organizan la UI
🌍 ¿Qué es Flutter?
Flutter es un framework de desarrollo cross-platform de Google que usa el lenguaje Dart. Con un único proyecto puedes compilar para Android, iOS, Windows, Linux, Web y macOS.
🔴 Desarrollo Nativo
Android → Kotlin/Java
iOS → Swift/Objective-C
✅ Mejor rendimiento, acceso directo al SO
🟡 Desarrollo Híbrido (Flutter)
Un solo proyecto → todos los SO
Dart = mezcla de JS + Kotlin + Python
✅ Un código para todo
Instalación
# 1. Descargar carpeta Flutter de flutter.dev → Instalar manualmente
# 2. Añadir Flutter a variables de entorno (PATH)
# 3. Verificar instalación:
flutter --version
# 4. Comprobar todo:
flutter doctor
# 5. En IntelliJ/Android Studio → Plugins → Marketplace → instalar "Flutter" y "Dart"bash
📦 Variables en Dart
Dart mezcla lo mejor de Kotlin y Java. Los tipos son obligatorios si no usas var. Los puntos y coma vuelven (igual que Java).
void main() {
// 1. Variable TIPADA (obligatorio si no usas var)
String nombre = 'Borja';
int edad = 41;
double salario = 3000.50;
bool activo = true;
// 2. late — inicialización diferida (como lateinit en Kotlin)
late String correo; // se puede declarar sin valor
correo = 'borja@ue.com'; // pero hay que asignarlo antes de usar
// 3. Null safety — variable que puede ser null (con ?)
String? direccion; // null por defecto
print(direccion ?? 'Sin dirección'); // operador ?? = Elvis de Kotlin
// 4. var DINÁMICO — Dart infiere el tipo, pero puede cambiar
var profesion = 'profesor'; // Dart lo tipa como String
profesion = true; // ¡puede cambiar a bool! Es dinámico
profesion = 100; // puede ser entero ahora
// 5. final y const (inmutables)
final ciudad = 'Madrid'; // se asigna en tiempo de ejecución (como val en Kotlin)
const gravedad = 9.8; // constante de compilación
// 6. String templates (igual que Kotlin con $)
print('Mi nombre es $nombre y tengo $edad años');
print('El salario es ${salario * 12} al año'); // expresiones con ${}
}dart
🔧 Funciones en Dart — Parámetros posicionales y nominales
En Dart los parámetros tienen formas de pasar que el profesor recalca mucho porque en Flutter se usan constantemente.
// ── FUNCIÓN CON PARÁMETROS POSICIONALES (como Java normal) ──
void saludar(String nombre, String apellido, int telefono) {
print('Bienvenido $nombre $apellido - Tel: $telefono');
}
// Llamada posicional: orden obligatorio
saludar('Borja', 'Martín', 111111);
// ── PARÁMETRO CON VALOR POR DEFECTO (posicional) ──
void saludarConDefault(String nombre, [int telefono = 0]) {
print('$nombre - Tel: $telefono');
}
saludarConDefault('Borja'); // telefono = 0 por defecto
saludarConDefault('Borja', 666000); // con teléfono
// ── PARÁMETROS NOMINALES OPCIONALES (con {}) ──
void saludarNominal({String? nombre, String? apellido, int telefono = 0}) {
print('$nombre $apellido - $telefono');
}
// Llamada nominal: puedes pasarlos en cualquier orden
saludarNominal(nombre: 'Borja', apellido: 'Martín');
saludarNominal(apellido: 'Martín', nombre: 'Borja'); // mismo resultado
// ── PARÁMETROS NOMINALES OBLIGATORIOS (required) ──
void saludarRequerido({required String nombre, required String apellido, int telefono = 0}) {
print('$nombre $apellido - $telefono');
}
// nombre y apellido son OBLIGATORIOS, telefono opcional
saludarRequerido(nombre: 'Borja', apellido: 'Martín');
// ── RETORNO de función ──
String obtenerSaludo(String nombre) {
return 'Hola $nombre';
}
// Sin tipo de retorno → retorna dynamic (cualquier cosa)
sinTipo() { return 42; } // tipo dynamicdart
Text('Hola', style: TextStyle(fontSize: 20))🏗️ Clases y Constructores en Dart
A diferencia de Kotlin (que tiene primario y secundarios), en Dart solo puedes tener un constructor principal. Para constructores adicionales se usan constructores nominales (con punto).
// ── Clase básica ──
class Usuario {
String nombre;
String apellido;
int edad;
// Constructor principal (único constructor "normal")
Usuario(this.nombre, this.apellido, this.edad);
// this.nombre = shorthand de: nombre = nombre;
void mostrarDatos() {
print('$nombre $apellido, $edad años');
}
}
// Crear objeto (vuelve el "new" de Java)
var u = new Usuario('Borja', 'Martín', 41);
u.mostrarDatos();
// ── CON PARÁMETRO NULLABLE (opcional) ──
class Alumno {
String nombre;
String apellido;
String? correo; // puede ser null
// Constructor principal — correo es opcional con valor por defecto
Alumno(this.nombre, this.apellido, {this.correo});
// Constructor NOMINAL — es como un "constructor secundario"
// Nombre: NombreClase.nombreDescriptivo(parámetros)
Alumno.conCorreo(String nombre, String apellido, String correo)
: this.nombre = nombre,
this.apellido = apellido,
this.correo = correo;
void mostrar() {
print('$nombre $apellido — ${correo ?? "Sin correo"}');
}
}
void main() {
// Constructor principal (sin correo)
var a1 = new Alumno('Ana', 'García');
a1.mostrar(); // "Ana García — Sin correo"
// Constructor nominal (con correo)
var a2 = Alumno.conCorreo('Luis', 'Pérez', 'luis@ue.com');
a2.mostrar(); // "Luis Pérez — luis@ue.com"
}dart
Kotlin: constructor
primario + constructor() secundarios que llaman a this()Dart: constructor
principal + NombreClase.nombreDescriptivo() nominales📱 Concepto de Widgets en Flutter
En Flutter no hay XML + Kotlin separados. Todo — gráfico y lógico — está en el mismo fichero Dart mediante Widgets (= Views de Android). Hay dos tipos:
StatelessWidget
Pantalla/componente que no cambia nunca. Sin variables que se actualicen. Como una pantalla estática.
StatefulWidget
Pantalla con estado. Tiene variables que pueden cambiar y re-pintar la UI con setState().
StatelessWidget — ejemplo mínimo
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Mi App',
home: Scaffold(
appBar: AppBar(title: const Text('Hola Flutter')),
body: const Center(
child: Text('Mi primera app', style: TextStyle(fontSize: 24)),
),
),
);
}
}dart
StatefulWidget — con estado y setState()
class Contador extends StatefulWidget {
const Contador({super.key});
@override
State<Contador> createState() => _ContadorState();
}
class _ContadorState extends State<Contador> {
// Variable de estado — cuando cambia, Flutter repinta el widget
int _contador = 0;
void _incrementar() {
setState(() { // setState() = notifica que hay cambios → repinta
_contador++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Contador')),
body: Center(
child: Text('Pulsaciones: $_contador', style: const TextStyle(fontSize: 32)),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementar,
child: const Icon(Icons.add),
),
);
}
}dart
📋 Lista dinámica con TextField + botón (patrón examen)
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ListaUsuarios()));
class ListaUsuarios extends StatefulWidget {
const ListaUsuarios({super.key});
@override
State<ListaUsuarios> createState() => _ListaUsuariosState();
}
class _ListaUsuariosState extends State<ListaUsuarios> {
// Controller para el TextField
final TextEditingController _ctrl = TextEditingController();
// Lista de usuarios (variable de estado)
final List<String> _usuarios = [];
void _validar() {
final texto = _ctrl.text.trim();
if (texto.isEmpty) {
// Mostrar Snackbar si está vacío
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Por favor introduce todos los datos')),
);
return;
}
// Añadir a lista y limpiar campo
setState(() {
_usuarios.add(texto);
_ctrl.clear();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Lista de Usuarios')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Campo de texto
TextField(
controller: _ctrl,
decoration: const InputDecoration(
labelText: 'Nombre del usuario',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
// Botón validar
ElevatedButton(
onPressed: _validar,
child: const Text('Validar'),
),
const Divider(),
// Lista de usuarios
Expanded(
child: ListView.builder(
itemCount: _usuarios.length,
itemBuilder: (ctx, i) => ListTile(
leading: const Icon(Icons.person),
title: Text(_usuarios[i]),
),
),
),
],
),
),
);
}
}dart
🏗️ Crear una segunda pantalla (navegación)
En Flutter para ir a otra pantalla se usa Navigator.push(). La nueva pantalla es un objeto Widget que se pasa como argumento.
// ─── Pantalla principal ───
class PantallaPrincipal extends StatelessWidget {
const PantallaPrincipal({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Principal')),
body: Center(
child: ElevatedButton(
// Navegar a segunda pantalla
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
// Instanciar la clase de la segunda pantalla
builder: (ctx) => const PantallaSecundaria(titulo: 'Detalles'),
),
),
child: const Text('Ir a segunda pantalla'),
),
),
);
}
}
// ─── Segunda pantalla ───
// Recibe parámetros por constructor (parámetros nominales)
class PantallaSecundaria extends StatelessWidget {
final String titulo;
const PantallaSecundaria({super.key, required this.titulo});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(titulo)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Pantalla: $titulo'),
ElevatedButton(
// Volver atrás
onPressed: () => Navigator.pop(context),
child: const Text('Volver'),
),
],
),
),
);
}
}
// ─── Navegar pasando datos ───
// Si la segunda pantalla necesita datos del objeto seleccionado:
class Producto {
final String nombre;
final double precio;
Producto(this.nombre, this.precio);
}
// Al pulsar un item de una lista:
// Navigator.push(context, MaterialPageRoute(
// builder: (ctx) => DetalleProducto(producto: _productos[i])
// ));
class DetalleProducto extends StatelessWidget {
final Producto producto; // recibe el OBJETO completo
const DetalleProducto({super.key, required this.producto});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(producto.nombre)),
body: Text('Precio: €${producto.precio}'),
);
}
}dart
required.🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Ejercicio típico: formulario de login, lista desde API y navegación a detalle. Son tres temas unidos.
FLUJO Pasos mentales
- Identificar tema principal
- Separar XML, lógica y datos
- Resolver primero lo mínimo que funcione
- Añadir mejoras al final
🎯 Mini ejercicio tipo examen
Monta un esquema de solución para una app con login, listado de productos y detalle del producto.
⚡ Resumen ultra rápido
- Primero que compile
- Luego que funcione
- Luego que quede bonito
- Escribe nombres claros y comenta poco pero útil
📋 Patrón del examen
Siempre 3 preguntas, eliges 2. Los patrones que se repiten año tras año:
| Pregunta | Tema | Lo que siempre piden |
|---|---|---|
| 1 | Android | XML dado o que hagas tú · Spinners/EditText · Snackbar · lógica de cálculo |
| 2 | Flutter | Lista dinámica con TextField + botón + ListView/Column |
| 3 | Unity | Movimiento WASD + colisiones + gravedad (siempre igual) |
🤖 EXAMEN 1 — Android: Calculadora de nota final
Tres Spinners (1-10), un EditText para la nota del examen, un botón. Fórmula: nota_final = examen*0.4 + sp1*0.2 + sp2*0.2 + sp3*0.2. Snackbar si el EditText está vacío o nota > 10.
activity_main.xml
<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView android:text="Nota Actividad 1" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<Spinner
android:id="@+id/spinnerSP1"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView android:text="Nota Actividad 2" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<Spinner
android:id="@+id/spinnerSP2"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView android:text="Nota Actividad 3" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<Spinner
android:id="@+id/spinnerSP3"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView android:text="Nota Examen (0-10)" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
<EditText
android:id="@+id/editExamen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:hint="Introduce tu nota del examen"/>
<Button
android:id="@+id/btnCalcular"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Calcular nota final"/>
</LinearLayout>xml
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var spinnerSP1: Spinner
private lateinit var spinnerSP2: Spinner
private lateinit var spinnerSP3: Spinner
private lateinit var editExamen: EditText
private lateinit var btnCalcular: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
spinnerSP1 = findViewById(R.id.spinnerSP1)
spinnerSP2 = findViewById(R.id.spinnerSP2)
spinnerSP3 = findViewById(R.id.spinnerSP3)
editExamen = findViewById(R.id.editExamen)
btnCalcular = findViewById(R.id.btnCalcular)
// Crear lista 1..10 para los spinners
val notas = (1..10).map { it.toString() }
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, notas)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinnerSP1.adapter = adapter
spinnerSP2.adapter = adapter
spinnerSP3.adapter = adapter
btnCalcular.setOnClickListener {
calcularNota()
}
}
private fun calcularNota() {
val examenStr = editExamen.text.toString().trim()
// Validación: campo vacío
if (examenStr.isEmpty()) {
Snackbar.make(btnCalcular, "Por favor introduce la nota del examen", Snackbar.LENGTH_LONG).show()
return
}
val notaExamen = examenStr.toDoubleOrNull() ?: 0.0
// Validación: nota mayor que 10
if (notaExamen > 10.0) {
Snackbar.make(btnCalcular, "La nota no puede ser mayor que 10", Snackbar.LENGTH_LONG).show()
return
}
val sp1 = spinnerSP1.selectedItem.toString().toDouble()
val sp2 = spinnerSP2.selectedItem.toString().toDouble()
val sp3 = spinnerSP3.selectedItem.toString().toDouble()
// Fórmula del examen
val notaFinal = notaExamen * 0.4 + sp1 * 0.2 + sp2 * 0.2 + sp3 * 0.2
Snackbar.make(btnCalcular,
"Tu nota final es: ${"%.2f".format(notaFinal)}",
Snackbar.LENGTH_LONG).show()
}
}kotlin
🤖 EXAMEN 2 — Android: Calculadora operaciones (examen anterior)
XML dado. Dos EditText con números, botón que calcula la operación y muestra resultado en Snackbar. Si algún campo vacío, Snackbar de error.
// MainActivity.kt — versión compacta con binding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnCalcular.setOnClickListener {
val n1Str = binding.editNumero1.text.toString().trim()
val n2Str = binding.editNumero2.text.toString().trim()
if (n1Str.isEmpty() || n2Str.isEmpty()) {
mostrarSnackbar("Por favor introduce todos los datos")
return@setOnClickListener
}
val n1 = n1Str.toDouble()
val n2 = n2Str.toDouble()
// Elige la operación según el spinner/radioButton del enunciado
val resultado = n1 + n2 // cambia por -, *, / según el examen
mostrarSnackbar("El resultado de la operación de suma entre $n1 y $n2 es $resultado")
}
}
private fun mostrarSnackbar(msg: String) {
Snackbar.make(binding.root, msg, Snackbar.LENGTH_LONG).show()
}
}kotlin
🐦 EXAMEN — Flutter: Lista dinámica de usuarios
TextField + botón Validar + ListView. Si vacío → Snackbar. Si relleno → añade a la lista.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Lista Usuarios',
home: const ListaUsuarios(),
);
}
class ListaUsuarios extends StatefulWidget {
const ListaUsuarios({super.key});
@override
State<ListaUsuarios> createState() => _ListaUsuariosState();
}
class _ListaUsuariosState extends State<ListaUsuarios> {
final TextEditingController _controller = TextEditingController();
final List<String> _usuarios = [];
void _validar() {
final nombre = _controller.text.trim();
if (nombre.isEmpty()) {
// Snackbar si vacío
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Por favor introduce todos los datos')),
);
return;
}
// Añadir a la lista y vaciar el campo
setState(() {
_usuarios.add(nombre);
_controller.clear();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Lista de Usuarios')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'Nombre del usuario',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: _validar,
child: const Text('Validar'),
),
const Divider(),
Expanded(
child: ListView.builder(
itemCount: _usuarios.length,
itemBuilder: (ctx, i) => ListTile(
leading: const Icon(Icons.person),
title: Text(_usuarios[i]),
),
),
),
],
),
),
);
}
}dart
🐦 EXAMEN — Flutter: Lista de productos (examen anterior)
Lista estática de al menos 5 productos con nombre y valor. Patrón más simple que el anterior.
class _ProductosState extends State<ListaProductos> {
// Lista estática de productos (nombre, precio)
final List<Map<String, dynamic>> _productos = [
{'nombre': 'Laptop', 'valor': 999.99},
{'nombre': 'Ratón', 'valor': 25.50},
{'nombre': 'Teclado', 'valor': 49.99},
{'nombre': 'Monitor', 'valor': 299.00},
{'nombre': 'Auriculares','valor': 79.99},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Productos')),
body: ListView.builder(
itemCount: _productos.length,
itemBuilder: (ctx, i) {
final p = _productos[i];
return ListTile(
leading: const Icon(Icons.shopping_cart),
title: Text(p['nombre']),
trailing: Text('€${p["valor"].toStringAsFixed(2)}',
style: const TextStyle(fontWeight: FontWeight.bold)),
);
},
),
);
}
}dart
🎮 EXAMEN — Unity: Movimiento WASD + colisiones + gravedad
Siempre el mismo ejercicio. Script para el personaje.
// PlayerController.cs — adjunta al GameObject del personaje
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float velocidad = 5f;
public float fuerzaSalto = 8f;
private Rigidbody2D rb;
private bool estaEnSuelo = false;
void Start()
{
rb = GetComponent<Rigidbody2D>();
// La gravedad la maneja el Rigidbody2D automáticamente
}
void Update()
{
// Movimiento horizontal: A (izq) y D (dcha)
float horizontal = Input.GetAxis("Horizontal"); // A/D o flechas
rb.velocity = new Vector2(horizontal * velocidad, rb.velocity.y);
// Salto: W o espacio (solo si está en el suelo)
if ((Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.Space)) && estaEnSuelo)
{
rb.velocity = new Vector2(rb.velocity.x, fuerzaSalto);
}
}
// Detectar si está en el suelo mediante colisión
void OnCollisionEnter2D(Collision2D col)
{
if (col.gameObject.CompareTag("Suelo"))
estaEnSuelo = true;
}
void OnCollisionExit2D(Collision2D col)
{
if (col.gameObject.CompareTag("Suelo"))
estaEnSuelo = false;
}
}
// PASOS en Unity:
// 1. Crear Sprite personaje → añadir Rigidbody2D + BoxCollider2D
// 2. Crear rectángulo suelo → añadir BoxCollider2D → Tag = "Suelo"
// 3. Adjuntar este script al personaje
// 4. En Rigidbody2D: Freeze Rotation Z = true (para que no rote)csharp
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Leer un CSV de alumnos, separar cada línea por comas y guardar el resultado en objetos.
FLUJO Pasos mentales
- Abrir flujo
- Leer contenido
- Transformar datos
- Cerrar recursos
🎯 Mini ejercicio tipo examen
Lee un fichero de texto con nombres y genera otro con los nombres en mayúsculas.
⚡ Resumen ultra rápido
- Flujo = canal de datos
- Siempre cierra recursos
- Texto y binario no se tratan igual
- Buffer mejora rendimiento
📦 Tipos de flujos
- Texto plano (.txt) → FileReader / FileWriter / PrintWriter
- Objetos (.obj) → ObjectOutputStream / ObjectInputStream (requiere Serializable)
- JSON (texto plano estructurado con clave-valor)
- XML (texto plano estructurado con etiquetas)
📖 Lectura de texto plano
// Proyecto Maven — sin dependencias externas necesarias
public void lecturaFichero(String path) {
File file = new File(path); // ruta del fichero
FileReader fr = null; // fuera del try para cerrarlo en finally
try {
fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
String linea;
while ((linea = br.readLine()) != null) {
System.out.println(linea);
}
} catch (IOException e) {
System.out.println("Error en la lectura");
} finally {
try {
if (fr != null) fr.close(); // se ejecuta SIEMPRE
} catch (IOException e) {
System.out.println("Error al cerrar");
}
}
}java
✏️ Escritura de texto plano
public void escrituraFichero(String path, String contenido) {
try (PrintWriter pw = new PrintWriter(new FileWriter(path))) {
// try-with-resources: cierra automáticamente al terminar
pw.println(contenido);
pw.println("Segunda línea");
} catch (IOException e) {
System.out.println("Error en la escritura");
}
}
// FileWriter con true = modo append (añadir sin borrar el fichero)
new FileWriter(path, true)java
🧩 Flujo de Objetos — Serialización
Para guardar/leer objetos completos en fichero, la clase debe implementar Serializable.
// La clase a serializar DEBE implementar Serializable
@Data @AllArgsConstructor @NoArgsConstructor
public class Usuario implements Serializable {
private static final long serialVersionUID = 1L; // ID de versión
private String nombre, correo;
private int edad;
}
// ─── ESCRITURA de objeto ───
public void escrituraObjeto(String path, Object obj) {
try (ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream(path))) {
oos.writeObject(obj);
} catch (IOException e) { System.out.println("Error escritura"); }
}
// ─── LECTURA de objeto ───
public Usuario lecturaObjeto(String path) {
Usuario u = null;
try (ObjectInputStream ois =
new ObjectInputStream(new FileInputStream(path))) {
u = (Usuario) ois.readObject(); // casteo obligatorio
} catch (IOException | ClassNotFoundException e) {
System.out.println("Error lectura objeto");
}
return u;
}
// También puedes guardar una lista entera:
oos.writeObject(listaUsuarios); // ArrayList<Usuario>
ArrayList<Usuario> lista = (ArrayList<Usuario>) ois.readObject();java
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Leer un JSON con una lista de productos y convertirlo a List<Producto> con ObjectMapper.
FLUJO Pasos mentales
- Crear clase modelo
- Crear
ObjectMapper - Leer o escribir JSON
- Comprobar nombres de campos
🎯 Mini ejercicio tipo examen
Convierte un objeto Alumno a JSON y luego vuelve a leerlo como objeto.
⚡ Resumen ultra rápido
- JSON puede ser objeto o array
- Los nombres de campos importan
- Jackson ahorra parseo manual
🔍 JSON Object vs JSON Array
// JSON Object → empieza con {
{ "nombre": "Borja", "edad": 30 }
// JSON Array → empieza con [
[{ "nombre": "Borja" }, { "nombre": "María" }]
// JSON con array anidado (típico de APIs)
{
"users": [ { "id": 1, "firstName": "Borja" }, ... ],
"total": 100,
"limit": 30
}json
{ → JSONObject. Si empieza con [ → JSONArray. Ábrelo en el navegador para verlo.📦 Dependencias Maven (pom.xml)
<!-- Jackson para mapear JSON <→> Objeto Java -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Lombok — getters, setters, constructores automáticos -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>LATEST</version>
</dependency>xml
🗺️ Mapeo JSON con Jackson
1. Crear la clase de respuesta (modelo)
// Los nombres de los atributos DEBEN coincidir exactamente con las claves del JSON
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true) // ignora campos extra del JSON
public class UsuarioResponse {
private List<UsuarioJSON> users; // "users" == clave del JSON
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class UsuarioJSON {
private int id;
private String firstName; // "firstName" == clave del JSON
private String lastName;
private String email;
}java
2. Hacer la petición y mapear
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
try {
URL url = new URL("https://dummyjson.com/users");
// mapper.readValue(url, ClaseQueRepresentaLaRespuesta.class)
UsuarioResponse response = mapper.readValue(url, UsuarioResponse.class);
// Ya tienes la lista lista para recorrer
for (UsuarioJSON user : response.getUsers()) {
System.out.println(user);
}
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}java
@JsonProperty("nombreEnJSON").🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
DAO de usuarios con métodos insertar, listar, actualizar y borrar.
FLUJO Pasos mentales
- Abrir conexión
- Preparar SQL
- Enviar parámetros
- Leer ResultSet
- Cerrar
🎯 Mini ejercicio tipo examen
Haz un DAO de productos con método buscarPorId.
⚡ Resumen ultra rápido
- PreparedStatement evita errores y mejora seguridad
- DAO separa responsabilidades
- ResultSet recorre filas
🏗️ Arquitectura general
JDBC es el conector que permite a Java hablar con cualquier base de datos SQL (MySQL, PostgreSQL, Oracle, SQLite...). Solo cambia la URL y el driver.
Dependencias en pom.xml
<!-- Conector MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>
<<!-- Lombok -->
<dependency>...lombok...</dependency>xml
🔗 Patrón Singleton — La conexión
El Singleton garantiza que solo existe UNA conexión a la base de datos, no 20000. Esto evita saturar el servidor.
public class DBConnection {
// static = pertenece a la clase, no al objeto. Solo hay uno.
private static Connection connection = null;
// Constructor privado — nadie puede hacer "new DBConnection()"
private DBConnection() {}
// Método público para obtener la conexión
public static Connection getConnection() {
if (connection == null) {
createConnection(); // solo se crea si no existe
}
return connection; // si ya existe, devuelve la misma
}
private static void createConnection() {
String user = "root";
String pass = "root";
// URL: jdbc:mysql://host:puerto/nombreDB
String url = "jdbc:mysql://127.0.0.1:3306/reservas_ue";
try {
connection = DriverManager.getConnection(url, user, pass);
System.out.println("Conexión OK");
} catch (SQLException e) {
System.out.println("Error conexión: " + e.getMessage());
}
}
}java
jdbc:mysql://. Para PostgreSQL sería jdbc:postgresql://. Todo lo demás es igual.⚡ Patrón DAO — Data Access Object
El DAO concentra toda la lógica SQL en una clase. Así la separas de la lógica de negocio.
public class UsuarioDAO {
private Connection conn = DBConnection.getConnection();
// ── INSERT ──
public void insertar(Usuario u) {
String sql = "INSERT INTO usuarios (nombre, correo, telefono) VALUES (?, ?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, u.getNombre());
ps.setString(2, u.getCorreo());
ps.setInt (3, u.getTelefono());
ps.executeUpdate();
} catch (SQLException e) { System.out.println("Error insert: " + e); }
}
// ── SELECT ALL ──
public List<Usuario> obtenerTodos() {
List<Usuario> lista = new ArrayList<>();
String sql = "SELECT * FROM usuarios";
try (PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
Usuario u = new Usuario(
rs.getInt("id"),
rs.getString("nombre"),
rs.getString("correo"),
rs.getInt("telefono")
);
lista.add(u);
}
} catch (SQLException e) { System.out.println("Error select: " + e); }
return lista;
}
// ── DELETE ──
public void borrar(int id) {
String sql = "DELETE FROM usuarios WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
ps.executeUpdate();
} catch (SQLException e) { System.out.println("Error delete: " + e); }
}
}java
PreparedStatement con ? en vez de concatenar strings. Evita SQL Injection y es más limpio.🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Entidad Cliente mapeada a la tabla clientes con @Entity, @Id y @Column.
FLUJO Pasos mentales
- Crear entidad
- Anotar campos
- Configurar persistencia
- Persistir o consultar
🎯 Mini ejercicio tipo examen
Mapea una clase Libro con id, titulo y precio usando anotaciones JPA.
⚡ Resumen ultra rápido
- ORM = objetos en vez de SQL manual
@Entitymarca la clase@Iddefine clave primaria
🤔 ¿Por qué ORM?
Con JDBC escribes SQL manualmente para insertar un objeto. Con un ORM (como Hibernate) le das el objeto y él genera el SQL solo.
No escribes INSERT INTO.... Solo haces session.persist(objeto) y Hibernate lo traduce.
📦 Dependencias Maven
<!-- Hibernate ORM -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.0.Final</version>
</dependency>
<<!-- Conector MySQL -->
<dependency>...mysql-connector-j...</dependency>
<<!-- Lombok -->
<dependency>...lombok...</dependency>xml
⚙️ hibernate.cfg.xml — Configuración obligatoria
Debe llamarse exactamente hibernate.cfg.xml y estar en src/main/resources.
<!-- hibernate.cfg.xml -->
<hibernate-configuration>
<session-factory>
<!-- Driver -->
<property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<!-- URL de conexión -->
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/hotel_ue</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">root</property>
<!-- create: crea tablas desde cero. update: actualiza si cambia la entidad -->
<property name="hibernate.hbm2ddl.auto">update</property>
<!-- Muestra el SQL generado en consola (muy útil para debug) -->
<property name="hibernate.show_sql">true</property>
<!-- Registra TODAS las clases que quieres mapear -->
<mapping class="com.empresa.model.Empleado"/>
<mapping class="com.empresa.model.Perfil"/>
</session-factory>
</hibernate-configuration>xml
<mapping class="..."/> aquí. Si no, Hibernate no la conoce.🏷️ Anotaciones JPA — El modelo
@Data // Lombok: genera getter, setter, toString, equals, hashCode
@AllArgsConstructor
@NoArgsConstructor
@Entity // JPA: esta clase es una entidad persistible
@Table(name = "empleados") // JPA: va a la tabla "empleados"
public class Empleado {
@Id // PK
@GeneratedValue(strategy = GenerationType.IDENTITY) // autoincremental
private Long id;
@Column(name = "nombre") // Si coincide el nombre, es opcional
private String nombre;
private String apellido; // sin @Column → usa el mismo nombre del atributo
@Column(unique = true)
private String correo;
private int salario;
@Transient // este campo NO va a la base de datos
private String campoTemporal;
}java
💾 DAO con Hibernate — CRUD
public class EmpleadoDAO {
// Obtener la SessionFactory del fichero de configuración
private SessionFactory getFactory() {
return new Configuration().configure().buildSessionFactory();
}
// ── INSERT ──
public void insertar(Empleado emp) {
Session session = null;
Transaction tx = null;
try {
session = getFactory().openSession();
tx = session.beginTransaction();
session.persist(emp); // INSERT automático
tx.commit();
} catch (Exception e) {
if (tx != null) tx.rollback();
} finally {
if (session != null) session.close();
}
}
// ── SELECT por ID ──
public Empleado buscarPorId(Long id) {
try (Session session = getFactory().openSession()) {
return session.find(Empleado.class, id); // SELECT + WHERE automático
}
}
// ── DELETE ──
public void borrar(Long id) {
try (Session session = getFactory().openSession()) {
Transaction tx = session.beginTransaction();
Empleado emp = session.find(Empleado.class, id);
if (emp != null) {
session.remove(emp); // DELETE automático
tx.commit();
System.out.println("Empleado borrado");
} else {
System.out.println("No encontrado");
}
}
}
// ── UPDATE — sin método especial: modifica el objeto y commit ──
public void actualizar(Long id, String nuevoCorreo) {
try (Session session = getFactory().openSession()) {
Transaction tx = session.beginTransaction();
Empleado emp = session.find(Empleado.class, id);
if (emp != null) {
emp.setCorreo(nuevoCorreo); // modificas el objeto
session.merge(emp); // UPDATE automático al commit
tx.commit();
}
}
}
}java
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Relacionar Cliente y Pedido con @OneToMany y @ManyToOne.
FLUJO Pasos mentales
- Elegir cardinalidad
- Poner la anotación correcta
- Definir lado propietario
- Probar inserción y lectura
🎯 Mini ejercicio tipo examen
Modela Profesor–Curso y explica si es uno a muchos o muchos a muchos.
⚡ Resumen ultra rápido
- OneToMany = uno tiene muchos
- ManyToOne = muchos apuntan a uno
- mappedBy evita duplicar relación
🔗 Los 3 tipos de relación
- @OneToOne → Un cliente tiene UNA dirección. Una dirección pertenece a UN cliente.
- @OneToMany / @ManyToOne → Un perfil tiene MUCHOS empleados. Un empleado tiene UN perfil.
- @ManyToMany → Un cliente puede tener MUCHOS empleados y viceversa. Necesita tabla auxiliar.
@ManyToOne en Empleado. "un perfil tiene muchos empleados" → @OneToMany en Perfil.1-N: @ManyToOne y @OneToMany
// ─── Clase EMPLEADO (lado "Many") ───
@Entity @Table(name = "empleados")
public class Empleado {
@Id @GeneratedValue(...) private Long id;
private String nombre;
// Muchos empleados → un perfil
@ManyToOne
@JoinColumn(name = "id_perfil") // columna FK en la tabla empleados
private Perfil perfil;
}
// ─── Clase PERFIL (lado "One") — bidireccional (opcional) ───
@Entity @Table(name = "perfiles")
public class Perfil {
@Id @GeneratedValue(...) private Long id;
private String nombre;
// Un perfil → lista de empleados (bidireccional — no obligatorio)
// mappedBy = nombre del atributo en la clase dominante (Empleado)
@OneToMany(mappedBy = "perfil")
private List<Empleado> empleados;
}java
N-N: @ManyToMany con tabla auxiliar
Necesitas crear manualmente la tabla auxiliar en la BD (ej: reservas con id_cliente y id_empleado).
// ─── Clase CLIENTE (entidad dominante) ───
@Entity @Table(name = "clientes")
public class Cliente {
@Id @GeneratedValue(...) private Long id;
private String nombre, correo;
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(
name = "reservas", // tabla auxiliar
joinColumns = @JoinColumn(name = "id_cliente"), // FK de ESTE lado
inverseJoinColumns = @JoinColumn(name = "id_empleado") // FK del OTRO lado
)
private Set<Empleado> empleados = new HashSet<>();
// Método para añadir una relación
public void addEmpleado(Empleado emp) {
empleados.add(emp);
}
}
// ─── Clase EMPLEADO (bidireccional — opcional) ───
@ManyToMany(mappedBy = "empleados") // mappedBy = atributo en Cliente
private Set<Cliente> clientes = new HashSet<>();java
@Data de Lombok y relaciones → riesgo de recursividad en toString(). Excluye los campos de relación del toString con @ToString.Exclude.📋 Resumen anotaciones JPA
- @Entity → la clase es una entidad persistible
- @Table(name="...") → nombre de la tabla en BD
- @Id → clave primaria
- @GeneratedValue(strategy=IDENTITY) → autoincremental
- @Column(name="...") → mapea a columna específica (opcional si mismo nombre)
- @Transient → atributo que NO va a la BD
- @Embeddable / @Embedded → clase embebida dentro de otra entidad
- @ManyToOne / @OneToMany / @OneToOne / @ManyToMany → relaciones
- @JoinColumn(name="...") → columna FK en relación simple
- @JoinTable(...) → tabla auxiliar en ManyToMany
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Proyecto con dependencia web y data; una clase principal arranca el servidor y ya puedes exponer endpoints.
FLUJO Pasos mentales
- Crear proyecto
- Añadir dependencias
- Definir entidades/servicios/controllers
- Ejecutar
🎯 Mini ejercicio tipo examen
Crea un proyecto Spring Boot que devuelva “API OK” en la ruta principal.
⚡ Resumen ultra rápido
- Spring Boot acelera configuración
- La clase principal arranca la app
- Las dependencias marcan capacidades
🏗️ ¿Qué es Spring Boot?
Spring Boot es un framework que convierte tu Java en un servidor web que recibe peticiones HTTP y las resuelve contra la base de datos. Es la capa intermedia del modelo de 3 capas.
(Android/Web/Java)
Spring Boot
(MySQL)
- El cliente ya no accede directamente a la BD → más seguro
- Cualquier cliente (Android, web, Flutter) usa la misma API
- Spring Boot levanta automáticamente un servidor Tomcat en el puerto 8080
🚀 Crear el proyecto
Como IntelliJ Community no crea proyectos Spring Boot, usa start.spring.io.
- Ve a start.spring.io
- Selecciona: Maven, Java, Spring Boot 3.x
- Pon el Group (ej:
com.ue) y Artifact (ej:tienda-ue) - Añade dependencias: Spring Web, Spring Data JPA, MySQL Driver, Rest Repositories
- Generate → descomprimir → abrir en IntelliJ
⚙️ application.properties — Configuración
En Spring Boot ya NO hay hibernate.cfg.xml. La configuración va en src/main/resources/application.properties.
# Conexión a la base de datos
spring.datasource.url=jdbc:mysql://localhost:3306/tienda_ue
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Hibernate — create, update, validate, none
spring.jpa.hibernate.ddl-auto=update
# Ver el SQL generado en consola
spring.jpa.show-sql=trueproperties
🗂️ Estructura del proyecto Spring Boot
⚙️ Crear proyecto Spring Boot desde cero
Usar start.spring.io para generar el proyecto. Dependencias necesarias: Spring Web, Spring Data JPA, el conector de tu BD.
pom.xml — dependencias mínimas
<dependencies>
<!-- Web (crea el servidor Tomcat + REST) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JPA + Hibernate (ORM) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Conector MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok (opcional pero muy útil: genera getters/setters) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>xml
application.properties
# Conexión a la base de datos
spring.datasource.url=jdbc:mysql://localhost:3306/mi_base
spring.datasource.username=root
spring.datasource.password=
# Hibernate — create|update|validate|none
spring.jpa.hibernate.ddl-auto=update
# Mostrar SQL generado en consola
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Puerto del servidor (8080 por defecto)
server.port=8080properties
🏗️ Estructura completa de un proyecto Spring Boot
src/main/java/com/ejemplo/
├── MiAplicacion.java ← @SpringBootApplication (main)
├── model/
│ └── Usuario.java ← @Entity
├── repository/
│ └── UsuarioRepository.java ← interface extends JpaRepository
├── service/
│ └── UsuarioService.java ← @Service (lógica de negocio)
└── controller/
└── UsuarioController.java ← @RestController (endpoints REST)bash
Main — punto de entrada
@SpringBootApplication
public class MiAplicacion {
public static void main(String[] args) {
SpringApplication.run(MiAplicacion.class, args);
// Esto levanta el servidor Tomcat en puerto 8080
}
}java
🔄 Flujo de una petición REST completa
// Cliente hace: GET http://localhost:8080/usuarios
//
// 1. El Controller recibe la petición
// 2. El Controller llama al Service
// 3. El Service aplica lógica y llama al Repository
// 4. El Repository hace la query SQL a la BD
// 5. La BD devuelve los datos → Repository → Service → Controller → JSON al cliente
// ─── ENTITY ───
@Entity @Table(name = "usuarios")
@Data @NoArgsConstructor @AllArgsConstructor
public class Usuario {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre, apellido, correo, pass;
}
// ─── REPOSITORY ───
@Repository
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
// JpaRepository da gratis: save(), findAll(), findById(), deleteById()
Optional<Usuario> findByCorreo(String correo); // Spring genera el SQL solo
}
// ─── SERVICE ───
@Service
public class UsuarioService {
@Autowired private UsuarioRepository repo;
public List<Usuario> listarTodos() { return repo.findAll(); }
public Usuario guardar(Usuario u) { return repo.save(u); }
public void borrar(Long id) { repo.deleteById(id); }
public Optional<Usuario> buscarPorId(Long id) { return repo.findById(id); }
}
// ─── CONTROLLER ───
@RestController
@RequestMapping("/usuarios")
public class UsuarioController {
@Autowired private UsuarioService service;
@GetMapping // GET /usuarios
public List<Usuario> listar() { return service.listarTodos(); }
@GetMapping("/{id}") // GET /usuarios/1
public Optional<Usuario> uno(@PathVariable Long id) {
return service.buscarPorId(id);
}
@PostMapping // POST /usuarios (body: JSON del usuario)
public Usuario crear(@RequestBody Usuario u) {
return service.guardar(u);
}
@PutMapping("/{id}") // PUT /usuarios/1
public Usuario actualizar(@PathVariable Long id, @RequestBody Usuario u) {
u.setId(id);
return service.guardar(u);
}
@DeleteMapping("/{id}") // DELETE /usuarios/1
public void borrar(@PathVariable Long id) {
service.borrar(id);
}
}java
curl -X POST http://localhost:8080/usuarios -H "Content-Type: application/json" -d '{"nombre":"Ana","correo":"ana@mail.com","pass":"1234"}'🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
ProductoRepository consulta la BD y ProductoService decide qué hacer antes de devolver resultados.
FLUJO Pasos mentales
- Crear repository
- Inyectarlo en service
- Crear métodos de negocio
- Llamarlo desde controller
🎯 Mini ejercicio tipo examen
Haz un servicio que devuelva solo productos con stock mayor que 0.
⚡ Resumen ultra rápido
- Repository = datos
- Service = lógica
- Controller = entrada/salida HTTP
🗃️ JpaRepository — CRUD gratuito
Extendiéndolo obtienes todos los métodos CRUD sin escribir nada.
// La interfaz extiende JpaRepository<TipoEntidad, TipoIdentificador>
public interface ClienteRepository extends JpaRepository<Cliente, Long> {
// Ya tienes GRATIS: findAll(), findById(), save(), deleteById()...
// Métodos adicionales — solo con la firma, Spring los implementa solo
List<Cliente> findByNombre(String nombre); // WHERE nombre = ?
List<Cliente> findByNombreAndCorreo(String n, String c); // WHERE nombre=? AND correo=?
Optional<Cliente> findByCorreo(String correo); // Optional porque puede no existir
// Si la firma no basta, usas @Query con HQL
@Query("SELECT c FROM Cliente c WHERE c.nombre LIKE %:nombre%")
List<Cliente> buscarPorNombreParcial(@Param("nombre") String nombre);
}java
findBy + nombre del atributo. Spring genera el SQL solo. ¡Sin escribir ninguna query!🧠 Servicio — La lógica de negocio
@Service // marca esta clase como servicio (inyectable)
public class ClienteService {
@Autowired // Spring inyecta el repositorio automáticamente
private ClienteRepository clienteRepository;
// Obtener todos
public List<Cliente> getAllClientes() {
return clienteRepository.findAll();
}
// Obtener por ID (devuelve Optional porque puede no existir)
public Optional<Cliente> getById(Long id) {
return clienteRepository.findById(id);
}
// Insertar (con lógica: comprobar si ya existe)
public Cliente addCliente(Cliente cliente) {
// Lógica antes de guardar
Optional<Cliente> existente = clienteRepository.findByCorreo(cliente.getCorreo());
if (existente.isPresent()) return null; // ya existe
return clienteRepository.save(cliente);
}
// Borrar
public boolean deleteCliente(Long id) {
if (clienteRepository.existsById(id)) {
clienteRepository.deleteById(id);
return true;
}
return false;
}
// Actualizar
public Cliente updateCliente(Cliente cliente) {
return clienteRepository.save(cliente); // save = insert si no existe, update si sí
}
}java
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Endpoint GET /productos para listar y POST /productos para crear.
FLUJO Pasos mentales
- Anotar con
@RestController - Definir ruta
- Usar
@GetMapping/@PostMapping - Devolver objeto o lista
🎯 Mini ejercicio tipo examen
Crea un controller de alumnos con un endpoint para listar y otro para buscar por id.
⚡ Resumen ultra rápido
- GET consulta
- POST crea
- PathVariable lee datos de la URL
- RequestBody recibe JSON
🌐 Métodos HTTP
- GET → Obtener datos (SELECT)
- POST → Crear datos (INSERT) — datos en el body
- PUT → Actualizar datos (UPDATE) — datos en el body
- DELETE → Borrar datos (DELETE)
Tipos de parámetros
- @PathVariable → en la URL:
/clientes/5 - @RequestParam → en la URL:
/clientes?nombre=Borja - @RequestBody → en el body (JSON) — para POST y PUT
🎮 RestController — Código completo
@RestController // esta clase gestiona peticiones HTTP
@RequestMapping("/api/clientes") // URL base de todos los endpoints
public class ClienteController {
@Autowired
private ClienteService clienteService;
// GET /api/clientes → devuelve todos los clientes
@GetMapping
public ResponseEntity<List<Cliente>> getAll() {
List<Cliente> clientes = clienteService.getAllClientes();
return ResponseEntity.ok(clientes); // 200 OK + datos
}
// GET /api/clientes/5 → devuelve el cliente con id=5
@GetMapping("/{id}")
public ResponseEntity<Cliente> getById(@PathVariable Long id) {
Optional<Cliente> cliente = clienteService.getById(id);
if (cliente.isPresent()) {
return ResponseEntity.ok(cliente.get()); // 200 OK
} else {
return ResponseEntity.notFound().build(); // 404 Not Found
}
}
// POST /api/clientes (body: {"nombre":"Borja","correo":"[email protected]"})
@PostMapping
public ResponseEntity<Cliente> add(@RequestBody Cliente cliente) {
Cliente guardado = clienteService.addCliente(cliente);
if (guardado != null) {
return ResponseEntity.status(201).body(guardado); // 201 Created
} else {
return ResponseEntity.status(409).build(); // 409 Conflict
}
}
// PUT /api/clientes/5 (body: {"nombre":"Borja","correo":"[email protected]"})
@PutMapping("/{id}")
public ResponseEntity<Cliente> update(@PathVariable Long id,
@RequestBody Cliente cliente) {
cliente.setId(id); // aseguramos que actualiza el correcto
Cliente actualizado = clienteService.updateCliente(cliente);
return ResponseEntity.ok(actualizado);
}
// DELETE /api/clientes/5
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
boolean borrado = clienteService.deleteCliente(id);
if (borrado) {
return ResponseEntity.ok().build(); // 200 OK
} else {
return ResponseEntity.notFound().build(); // 404 Not Found
}
}
// GET /api/clientes?nombre=Borja → filtrar por nombre
@GetMapping("/buscar")
public List<Cliente> buscar(@RequestParam String nombre) {
return clienteService.buscarPorNombre(nombre);
}
}java
🧪 Probar la API con Thunder Client
- Instala la extensión Thunder Client en VS Code
- Arranca el servidor Spring Boot (dale al play en IntelliJ)
- Crea una nueva petición:
GET http://localhost:8080/api/clientes - Para POST/PUT: ve a la pestaña Body → JSON y escribe el objeto
// Ejemplo body para POST /api/clientes
{
"nombre": "Borja",
"correo": "[email protected]",
"telefono": 123456789
}
// Códigos de respuesta importantes:
// 200 OK → todo bien
// 201 Created → creado con éxito
// 404 Not Found → no encontrado
// 409 Conflict → ya existe
// 500 Error → algo falló en el servidorjson
📋 Resumen anotaciones Spring Boot
- @SpringBootApplication → clase principal que arranca el servidor
- @Entity / @Table → modelo JPA (igual que Hibernate)
- @Repository → interfaz repositorio (o simplifica extendie JpaRepository)
- @Service → clase de lógica de negocio
- @RestController → clase que gestiona endpoints HTTP
- @RequestMapping("/ruta") → URL base del controlador
- @GetMapping / @PostMapping / @PutMapping / @DeleteMapping → tipo de petición
- @PathVariable → parámetro en la URL (
/clientes/{id}) - @RequestParam → parámetro en la query (
?nombre=Borja) - @RequestBody → cuerpo JSON de la petición
- @Autowired → inyección automática de dependencias
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Leer JSON, convertirlo a objetos y guardarlo en BD con DAO o repository.
FLUJO Pasos mentales
- Detectar tecnología pedida
- Diseñar modelo
- Implementar acceso
- Probar caso feliz
🎯 Mini ejercicio tipo examen
Describe cómo resolverías una importación de alumnos desde un fichero JSON a una base de datos.
⚡ Resumen ultra rápido
- No mezcles capas
- Usa nombres claros
- Comprueba errores de conexión y formato
📋 Patrón del examen
| Pregunta | Tema | Lo que siempre piden |
|---|---|---|
| 1 | Ficheros/JSON URL | Leer TXT línea a línea ó leer JSON desde URL con org.json + Maven |
| 2 | Hibernate | Completar configuración: entidad + anotaciones + hibernate.cfg.xml + insertar |
| 3 | Spring Boot | CRUD completo: entidad + repository + controller con endpoints REST |
📄 EXAMEN — Leer fichero .txt línea a línea (examen anterior)
// Proyecto Java normal (sin Maven necesario)
import java.io.*;
import java.nio.file.*;
public class LeerFichero {
public static void main(String[] args) {
// OPCIÓN 1 — BufferedReader (más habitual)
try (BufferedReader br = new BufferedReader(new FileReader("ejercicio_uno.txt"))) {
String linea;
while ((linea = br.readLine()) != null) {
System.out.println(linea);
}
} catch (IOException e) {
System.out.println("Error al leer: " + e.getMessage());
}
// OPCIÓN 2 — Files.readAllLines (más corto)
try {
List<String> lineas = Files.readAllLines(Path.of("ejercicio_uno.txt"));
lineas.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}java
🌐 EXAMEN — JSON desde URL con org.json (ambos años)
pom.xml con la dependencia org.json, leer de https://dummyjson.com/products, mostrar nombre y precio.
pom.xml
<!-- pom.xml -->
<project>
<groupId>com.examen</groupId>
<artifactId>leer-json</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20220924</version>
</dependency>
</dependencies>
</project>xml
Main.java
import org.json.*;
import java.net.*;
import java.io.*;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
try {
// 1. Conectar a la URL y leer el JSON
URL url = new URL("https://dummyjson.com/products");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
Scanner sc = new Scanner(conn.getInputStream());
StringBuilder sb = new StringBuilder();
while (sc.hasNext()) sb.append(sc.nextLine());
sc.close();
// 2. Parsear el JSON
JSONObject raiz = new JSONObject(sb.toString());
JSONArray products = raiz.getJSONArray("products");
// 3. Iterar y mostrar nombre + precio
for (int i = 0; i < products.length(); i++) {
JSONObject producto = products.getJSONObject(i);
String nombre = producto.getString("title");
double precio = producto.getDouble("price");
System.out.printf("Producto: %-30s | Precio: %.2f$%n", nombre, precio);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}java
🗄️ EXAMEN — Hibernate: Entidad + insertar (año actual)
Tabla Usuarios con FK a habitaciones. Completar configuración.
hibernate.cfg.xml
<!-- src/main/resources/hibernate.cfg.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mi_base</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password"></property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQL8Dialect</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<property name="hibernate.show_sql">true</property>
<!-- Mapear TODAS las entidades aquí -->
<mapping class="com.examen.model.Habitacion"/>
<mapping class="com.examen.model.Cliente"/>
</session-factory>
</hibernate-configuration>xml
Habitacion.java
@Entity
@Table(name = "habitaciones")
public class Habitacion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "numero")
private int numero;
@Column(name = "planta")
private int planta;
@Column(name = "capacidad")
private int capacidad;
// Constructor vacío obligatorio para Hibernate
public Habitacion() {}
public Habitacion(int numero, int planta, int capacidad) {
this.numero = numero;
this.planta = planta;
this.capacidad = capacidad;
}
// getters y setters...
}java
Cliente.java (con FK a Habitacion)
@Entity
@Table(name = "Usuarios")
public class Cliente {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "nombre")
private String nombre;
@Column(name = "apellido")
private String apellido;
@Column(name = "telefono")
private int telefono;
@Column(name = "ciudad")
private String ciudad;
// FK: relación ManyToOne con Habitacion
@ManyToOne
@JoinColumn(name = "habitacion_id")
private Habitacion habitacion;
public Cliente() {}
// constructor con todos los campos, getters y setters...
}
// ─── Main para insertar ───
public class Main {
public static void main(String[] args) {
SessionFactory factory = new Configuration().configure().buildSessionFactory();
try (Session session = factory.openSession()) {
Transaction tx = session.beginTransaction();
// Primero insertar (o buscar) la habitación
Habitacion hab = new Habitacion(101, 1, 2);
session.persist(hab);
// Luego insertar el cliente con la FK
Cliente c = new Cliente();
c.setNombre("Juan");
c.setApellido("García");
c.setTelefono(666111222);
c.setCiudad("Madrid");
c.setHabitacion(hab); // asignar la FK
session.persist(c);
tx.commit();
System.out.println("Insertado correctamente");
}
}
}java
🌱 EXAMEN — Spring Boot: CRUD usuarios completo
1. pom.xml (dependencias clave)
<dependencies>
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId></dependency>
</dependencies>xml
2. application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/mi_base
spring.datasource.username=root
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=trueproperties
3. Usuario.java (entidad)
@Entity
@Table(name = "usuarios")
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nombre;
private String apellido;
private String correo;
private String pass;
public Usuario() {}
// Constructor, getters y setters de todos los campos
public Long getId() { return id; }
public String getNombre() { return nombre; }
public void setNombre(String n) { this.nombre = n; }
public String getApellido() { return apellido; }
public void setApellido(String a) { this.apellido = a; }
public String getCorreo() { return correo; }
public void setCorreo(String c) { this.correo = c; }
public String getPass() { return pass; }
public void setPass(String p) { this.pass = p; }
}java
4. UsuarioRepository.java
@Repository
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
// JpaRepository ya da: save(), findAll(), findById(), deleteById()
// No necesitas añadir nada para el examen básico
}java
5. UsuarioController.java (los 3 endpoints del examen)
@RestController
@RequestMapping("/usuarios")
public class UsuarioController {
@Autowired
private UsuarioRepository repo;
// a) Agregar usuario → POST /usuarios
@PostMapping
public Usuario agregar(@RequestBody Usuario u) {
return repo.save(u);
}
// b) Borrar por id → DELETE /usuarios/{id}
@DeleteMapping("/{id}")
public void borrar(@PathVariable Long id) {
repo.deleteById(id);
}
// c) Listar todos → GET /usuarios
@GetMapping
public List<Usuario> listar() {
return repo.findAll();
}
}
// Probar con curl o Postman:
// POST http://localhost:8080/usuarios body: {"nombre":"Ana","apellido":"López","correo":"ana@mail.com","pass":"1234"}
// GET http://localhost:8080/usuarios
// DELETE http://localhost:8080/usuarios/1java
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Proyecto con paquetes para vistas, controladores y modelos.
FLUJO Pasos mentales
- Crear estructura de paquetes
- Separar ventanas y lógica
- Reutilizar componentes
🎯 Mini ejercicio tipo examen
Organiza una app de biblioteca en paquetes MVC.
⚡ Resumen ultra rápido
- Vista muestra
- Controlador responde
- Modelo guarda datos
🗺️ Las 3 partes del curso
① Swing + WindowBuilder
Plugin de Eclipse. Arrastrar y soltar. La tecnología más antigua pero sigue siendo base de examen.
② JavaFX + SceneBuilder
Librería moderna de Java. Ficheros FXML + clase Controller. Más potente y flexible.
③ Python + Qt Designer
Entorno gráfico en Python. Ficheros .ui + código .py. Utiliza pymysql para conectar con MySQL.
🏗️ Patrón Modelo-Vista-Controlador (MVC)
Estructura obligatoria para organizar el proyecto en paquetes:
// Estructura de paquetes recomendada
mi.proyecto/
├── model/ // Clases molde: Alumno.java, Profesor.java...
├── view/ (gráficos) // Ventanas: LoginFrame.java, PrincipalFrame.java...
├── database/ (dao/io) // Conexión BD + métodos SQL
├── controller/ // Intermediario: recibe objeto del model, llama a BD
└── Main.java // UN solo main → crea y muestra las ventanasestructura
🏗️ Estructura del proyecto (MVC)
Siempre usar el patrón Modelo–Vista–Controlador. Separar en paquetes:
| Paquete | Qué contiene | Ejemplo |
|---|---|---|
model | Clases molde (POJO) | Alumno.java, Producto.java |
view | Ventanas Swing/JavaFX | VentanaPrincipal.java |
controller | Lógica intermedia | AlumnoController.java |
dao / db | Acceso a base de datos | ConexionBD.java, AlumnoDAO.java |
Clase modelo básica (POJO)
public class Alumno {
private int id;
private String nombre;
private String apellido;
public Alumno() {}
public Alumno(int id, String nombre, String apellido) {
this.id = id; this.nombre = nombre; this.apellido = apellido;
}
public int getId() { return id; }
public String getNombre() { return nombre; }
public String getApellido() { return apellido; }
public void setNombre(String n) { nombre = n; }
public void setApellido(String a) { apellido = a; }
}java
🪟 Crear una ventana JFrame desde cero
import javax.swing.*;
import java.awt.*;
public class VentanaPrincipal extends JFrame {
private JTextField txtNombre;
private JButton btnAceptar;
private JLabel lblResultado;
public VentanaPrincipal() {
setTitle("Mi Ventana");
setSize(400, 300);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null); // centrar en pantalla
setLayout(new FlowLayout());
// Crear componentes
txtNombre = new JTextField(20);
btnAceptar = new JButton("Aceptar");
lblResultado = new JLabel("Resultado aquí");
// Añadir componentes a la ventana
add(new JLabel("Nombre:"));
add(txtNombre);
add(btnAceptar);
add(lblResultado);
// Listener del botón
btnAceptar.addActionListener(e -> {
String nombre = txtNombre.getText().trim();
lblResultado.setText("Hola, " + nombre + "!");
});
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() ->
new VentanaPrincipal().setVisible(true));
}
}java
📐 Layouts principales en Swing
| Layout | Comportamiento | Cuándo usarlo |
|---|---|---|
FlowLayout | Izq → dcha, nueva fila si no cabe | Botones simples |
BorderLayout | 5 zonas: N, S, E, W, CENTER | Estructura principal de ventana |
GridLayout(f,c) | Cuadrícula fija f×c | Formularios regulares |
GridBagLayout | Cuadrícula flexible | Formularios complejos |
AbsoluteLayout / null | Posición manual x,y | WindowBuilder arrastra-suelta |
// Ejemplo BorderLayout: toolbar arriba, lista en centro, botones abajo
setLayout(new BorderLayout(5, 5));
add(pnlFiltros, BorderLayout.NORTH);
add(new JScrollPane(tabla), BorderLayout.CENTER);
add(pnlBotones, BorderLayout.SOUTH);
// Ejemplo GridLayout: formulario 4 filas × 2 columnas
JPanel form = new JPanel(new GridLayout(4, 2, 5, 5));
form.add(new JLabel("Nombre:")); form.add(txtNombre);
form.add(new JLabel("Apellido:")); form.add(txtApellido);
form.add(new JLabel("DNI:")); form.add(txtDni);
form.add(new JLabel("Edad:")); form.add(txtEdad);java
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Ventana con formulario usando JFrame, JPanel, JLabel, JTextField y JButton.
FLUJO Pasos mentales
- Crear ventana
- Elegir layout
- Añadir componentes
- Hacer visible
🎯 Mini ejercicio tipo examen
Diseña un formulario de alta de libro con título, autor y botón guardar.
⚡ Resumen ultra rápido
- JFrame = ventana
- JPanel = contenedor
- Layout organiza
- pack() ajusta tamaño
🪟 JFrame vs JPanel
- JFrame → Es la ventana. Puede abrirse sola. Siempre obligatoria como mínimo 1.
- JPanel → Es un contenedor. No puede abrirse solo, siempre vive dentro de un JFrame.
- JDialog → Ventana modal (cuadro de diálogo).
// Main.java — UN solo main
public class Main {
public static void main(String[] args) {
LoginFrame login = new LoginFrame();
login.setVisible(true); // sin esto no aparece la ventana
}
}java
📐 Layouts — Cómo distribuir los elementos
- AbsoluteLayout → Posición libre con coordenadas X,Y. El más fácil pero no responsive.
- BorderLayout → 5 zonas: NORTH, SOUTH, CENTER, EAST, WEST. Muy útil para ventanas grandes.
- FlowLayout → Elementos en fila de izquierda a derecha.
- GridLayout → Cuadrícula de filas y columnas iguales.
- GridBagLayout → Cuadrícula avanzada con pesos. Muy potente, más complejo.
🧩 Componentes principales
Texto y entrada
- JLabel → Texto estático o imagen
- JTextField → Campo de texto (tf→ nombre: tfUsuario)
- JPasswordField → Campo contraseña (pfContra). Cambiar eco:
setEchoChar('*') - JTextArea → Área de texto multilínea
Acción y selección
- JButton → Botón (btn→ nombre: btnEntrar)
- JComboBox → Lista desplegable
- JCheckBox → Casilla múltiple
- JRadioButton → Selección única (agrupar con ButtonGroup)
- JTable → Tabla de datos (dentro de JScrollPane)
🖼️ Añadir imagen a un JLabel
- Crea carpeta
imagenesdentro desrc(botón derecho → New Folder). - Copia la imagen en esa carpeta.
- En el JLabel, ve a Properties → Icon → Classpath → selecciona la imagen.
- Redimensiona la imagen previamente al tamaño del Label (el Label no auto-escala).
// Por código:
ImageIcon icon = new ImageIcon(getClass().getResource("/imagenes/logo.png"));
lblImagen.setIcon(icon);java
📑 JTabbedPane — Múltiples paneles en una ventana
En vez de abrir/cerrar ventanas, usa pestañas con un JTabbedPane.
// Por código:
JTabbedPane tabPanel = new JTabbedPane();
tabPanel.addTab("Alumnos", panelAlumnos);
tabPanel.addTab("Profesores", panelProfesores);
add(tabPanel, BorderLayout.CENTER);java
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Botón que lee dos cajas de texto y muestra el resultado en una etiqueta.
FLUJO Pasos mentales
- Crear componente
- Asociar listener
- Leer valores
- Actualizar interfaz
🎯 Mini ejercicio tipo examen
Haz un botón “Limpiar” que vacíe todos los campos de un formulario.
⚡ Resumen ultra rápido
- ActionListener escucha clics
- El evento dispara la lógica
- La vista se actualiza al final
👆 ActionListener — El evento más común
Método preferido del profe: implementar ActionListener en la clase de la ventana y centralizar todos los listeners.
public class LoginFrame extends JFrame implements ActionListener {
// Declarar botones FUERA del constructor (scope global)
private JButton btnEntrar;
private JButton btnSalir;
private JButton btnLimpiar;
private JTextField tfUsuario;
private JPasswordField pfContra;
public LoginFrame() {
initComponents(); // inicializar gráfico
initListeners(); // añadir listeners
}
private void initListeners() {
btnEntrar.addActionListener(this);
btnSalir.addActionListener(this);
btnLimpiar.addActionListener(this);
}
// UN solo método gestiona TODOS los eventos
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == btnEntrar) {
comprobarLogin();
} else if (e.getSource() == btnSalir) {
System.exit(0);
} else if (e.getSource() == btnLimpiar) {
tfUsuario.setText("");
pfContra.setText("");
}
}
}java
actionPerformed. En WindowBuilder: botón derecho → Expose Component.🔄 Abrir una ventana nueva desde un botón
// Dentro del actionPerformed:
if (e.getSource() == btnRegistro) {
RegistroFrame registro = new RegistroFrame();
registro.setVisible(true);
this.dispose(); // cierra la ventana actual (opcional)
// o this.setVisible(false); para ocultarla sin cerrarla
}java
📋 JComboBox — Lista desplegable
// Añadir elementos estáticos:
JComboBox<String> cbRol = new JComboBox<>();
cbRol.addItem("Alumno");
cbRol.addItem("Profesor");
// Añadir desde base de datos (en un bucle):
while (rs.next()) {
cbRol.addItem(rs.getString("nombre"));
}
// Obtener el valor seleccionado:
String rolElegido = (String) cbRol.getSelectedItem();java
🧪 En práctica — cómo pensarlo
JTable.CASO REAL Ejemplo de uso
Cargar usuarios desde MySQL y mostrarlos en un DefaultTableModel.
FLUJO Pasos mentales
- Consultar datos
- Construir modelo de tabla
- Asignar a JTable
- Refrescar
🎯 Mini ejercicio tipo examen
Muestra alumnos de una BD en una tabla con columnas id, nombre y nota.
⚡ Resumen ultra rápido
- JTable no guarda datos por sí sola
- El modelo manda
- Recargar tabla = volver a consultar o limpiar y añadir filas
🔌 Añadir conector MySQL (Eclipse)
- Descarga el conector MySQL JDBC (.jar) desde mysql.com.
- Crea carpeta
lib(olibreria) dentro de tu proyecto. - Copia el .jar ahí dentro.
- Botón derecho en el proyecto → Build Path → Configure Build Path → Libraries → Add External JARs → selecciónalo.
🗄️ Clase Conexión
public class ConexionBD {
private static final String URL = "jdbc:mysql://localhost:3306/mi_base";
private static final String USER = "root";
private static final String PASS = ""; // vacío por defecto en XAMPP
public static Connection conectar() {
Connection con = null;
try {
con = DriverManager.getConnection(URL, USER, PASS);
} catch (SQLException e) {
System.out.println("Error conexión: " + e.getMessage());
}
return con;
}
public static void desconectar(Connection con) {
try {
if (con != null) con.close();
} catch (SQLException e) { System.out.println("Error cierre"); }
}
}java
📊 Clase Gestión BD — SELECT e INSERT
public class GestionBD {
// ── SELECT: comprobar login ──
public boolean comprobarLogin(String usuario, String pass) {
Connection con = ConexionBD.conectar();
boolean ok = false;
try {
String sql = "SELECT * FROM usuarios WHERE usuario=? AND password=?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, usuario);
ps.setString(2, pass);
ResultSet rs = ps.executeQuery();
ok = rs.next(); // true si hay resultado
rs.close(); ps.close();
} catch (SQLException e) { System.out.println(e); }
ConexionBD.desconectar(con);
return ok;
}
// ── SELECT: cargar datos en la tabla ──
public DefaultTableModel obtenerAlumnos() {
String[] cols = {"ID", "Nombre", "Nota"};
DefaultTableModel model = new DefaultTableModel(cols, 0);
Connection con = ConexionBD.conectar();
try {
PreparedStatement ps = con.prepareStatement("SELECT * FROM alumnos");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Object[] fila = {
rs.getInt("id"),
rs.getString("nombre"),
rs.getDouble("nota")
};
model.addRow(fila);
}
} catch (SQLException e) { System.out.println(e); }
ConexionBD.desconectar(con);
return model;
}
// ── INSERT ──
public int insertarAlumno(String nombre, double nota) {
Connection con = ConexionBD.conectar();
int result = 0;
try {
String sql = "INSERT INTO alumnos (nombre, nota) VALUES (?, ?)";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, nombre);
ps.setDouble(2, nota);
result = ps.executeUpdate(); // devuelve 1 si OK, 0 si fallo
} catch (SQLException e) { System.out.println(e); }
ConexionBD.desconectar(con);
return result;
}
}java
PreparedStatement con ?, nunca concatenes el SQL con los valores del usuario. Evita inyección SQL y es más limpio.📋 JTable — Cargar datos desde BD
// Siempre dentro de un JScrollPane
JScrollPane scroll = new JScrollPane();
JTable tblAlumnos = new JTable();
scroll.setViewportView(tblAlumnos);
// Cargar datos desde BD:
GestionBD gbd = new GestionBD();
DefaultTableModel modelo = gbd.obtenerAlumnos();
tblAlumnos.setModel(modelo);java
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Crear una escena, cargar un layout y mostrar una ventana inicial.
FLUJO Pasos mentales
- Crear stage
- Cargar scene
- Asignar root
- Mostrar ventana
🎯 Mini ejercicio tipo examen
Haz una app JavaFX que abra una ventana con un botón y un texto.
⚡ Resumen ultra rápido
- Stage = ventana
- Scene = escena
- FXML puede definir la vista
🧩 Cómo funciona JavaFX
JavaFX separa el diseño (FXML) de la lógica (Controller). El SceneBuilder edita el FXML visualmente y lo guarda automáticamente.
login.fxml
Define la interfaz gráfica. Editar con SceneBuilder. No tocar a mano salvo que controles bien XML.
LoginController.java
Contiene los métodos que hacen las acciones de los botones. Usa anotaciones @FXML.
// Estructura de proyecto JavaFX
src/
├── main/java/com/ue/
│ ├── Launcher.java // lanza la app
│ ├── HelloApplication.java // carga el FXML y abre la ventana
│ ├── controller/
│ │ └── LoginController.java
│ ├── database/
│ │ └── ConexionBD.java
│ └── model/
│ └── Alumno.java
└── main/resources/
├── fxml/
│ └── login.fxml
└── images/
└── logo.pngestructura
🚀 HelloApplication.java — Clase que lanza la app
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws Exception {
// Carga el FXML — ¡la ruta es crítica!
FXMLLoader fxmlLoader = new FXMLLoader(
getClass().getResource("/fxml/login.fxml")
);
Scene scene = new Scene(fxmlLoader.load());
stage.setTitle("Mi Aplicación");
stage.setResizable(false);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}java
📦 Añadir MySQL en IntelliJ (pom.xml)
<!-- pom.xml — dentro de <dependencies> -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>xml
java.sql.* no funciona, abre module-info.java y añade: requires java.sql;🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Formulario diseñado visualmente y conectado a un controller que responde a un botón.
FLUJO Pasos mentales
- Diseñar en SceneBuilder
- Asignar fx:id
- Indicar controller
- Programar acciones
🎯 Mini ejercicio tipo examen
Haz un login en SceneBuilder con usuario, contraseña y botón entrar.
⚡ Resumen ultra rápido
- fx:id conecta componente y código
- El controller centraliza eventos
- No olvides enlazar el FXML correcto
🔗 Conectar FXML con su Controller
En SceneBuilder, abajo a la izquierda, en la sección Controller, escribe el nombre completo de la clase:
<!-- En el FXML generado automáticamente: -->
<AnchorPane fx:controller="com.ue.controller.LoginController">
...
</AnchorPane>fxml
.java. Error muy común: poner LoginController.java en vez de LoginController.🏷️ Identificar elementos (FX:ID y OnAction)
En SceneBuilder, pestaña Code de cada elemento:
- fx:id → nombre del campo en el Controller (para leer/escribir su valor)
- On Action → nombre del método que se ejecuta al pulsarlo
// LoginController.java
public class LoginController {
// @FXML vincula el campo con el fx:id del SceneBuilder
@FXML private TextField tfUsuario;
@FXML private PasswordField pfContra;
@FXML private ComboBox<String> cbRol;
// @FXML en el método → vinculado con el On Action del botón
@FXML
public void onEntrar(ActionEvent event) {
String usuario = tfUsuario.getText();
String contra = pfContra.getText();
// lógica de login...
}
@FXML
public void onSalir(ActionEvent event) {
Platform.exit();
}
@FXML
public void onLimpiar(ActionEvent event) {
tfUsuario.clear();
pfContra.clear();
}
}java
📐 Contenedores más usados en SceneBuilder
- AnchorPane → Posición libre (como AbsoluteLayout en Swing). El más cómodo para empezar.
- BorderPane → 5 zonas (Top, Bottom, Center, Left, Right). Responsive al redimensionar.
- VBox → Apila elementos en vertical.
- HBox → Alinea elementos en horizontal (útil para botones).
- GridPane → Cuadrícula de filas y columnas.
- ScrollPane → Contenedor con scroll (usar con TableView).
- TabPane → Pestañas (como JTabbedPane en Swing).
🎨 Estilo CSS en JavaFX
JavaFX permite aplicar estilos CSS a los elementos, algo que Swing no hace bien.
/* styles.css — en carpeta resources/css/ */
.root {
-fx-background-color: #1a1a2e;
-fx-font-family: "Arial";
}
.button {
-fx-background-color: #e94560;
-fx-text-fill: white;
-fx-border-radius: 8px;
}
#btnEntrar { /* por ID */
-fx-font-size: 14px;
-fx-font-weight: bold;
}css
// Cargar el CSS en la Scene:
scene.getStylesheets().add(getClass().getResource("/css/styles.css").toExternalForm());java
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Login que consulta usuario en BD y, si es correcto, abre otra ventana.
FLUJO Pasos mentales
- Leer usuario y contraseña
- Consultar BD
- Decidir si acceso válido
- Abrir nueva scene o stage
🎯 Mini ejercicio tipo examen
Haz un login que, al acertar, abra una pantalla de bienvenida con el nombre del usuario.
⚡ Resumen ultra rápido
- Valida antes de cambiar de ventana
- No metas SQL dispersa por la interfaz
🔐 Login completo con BD en JavaFX
// LoginController.java
public class LoginController {
@FXML private TextField tfUsuario;
@FXML private PasswordField pfContra;
@FXML private Label lblError;
@FXML
public void onEntrar(ActionEvent e) {
String usuario = tfUsuario.getText();
String contra = pfContra.getText();
if (usuario.isEmpty() || contra.isEmpty()) {
lblError.setText("Rellena todos los campos");
return;
}
boolean ok = new GestionBD().comprobarLogin(usuario, contra);
if (ok) {
abrirVentanaPrincipal(e);
} else {
lblError.setText("Usuario o contraseña incorrectos");
}
}
private void abrirVentanaPrincipal(ActionEvent e) {
try {
FXMLLoader loader = new FXMLLoader(
getClass().getResource("/fxml/principal.fxml")
);
Scene scene = new Scene(loader.load());
Stage stage = (Stage) ((Node) e.getSource()).getScene().getWindow();
stage.setScene(scene);
stage.show();
} catch (Exception ex) { ex.printStackTrace(); }
}
}java
📊 TableView con datos de BD
// En el FXML (SceneBuilder): añade TableView con sus TableColumn
// fx:id del TableView: tblAlumnos
// fx:id de las columnas: colNombre, colNota
@FXML private TableView<Alumno> tblAlumnos;
@FXML private TableColumn<Alumno, String> colNombre;
@FXML private TableColumn<Alumno, Double> colNota;
public void initialize() { // se ejecuta al cargar el FXML
// Vincular columnas con atributos del modelo
colNombre.setCellValueFactory(new PropertyValueFactory<>("nombre"));
colNota.setCellValueFactory(new PropertyValueFactory<>("nota"));
// Cargar datos
ObservableList<Alumno> lista = new GestionBD().getAlumnos();
tblAlumnos.setItems(lista);
}java
Alumno necesita getters para que PropertyValueFactory funcione. Usa Lombok @Getter o genera los getters normalmente.🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Diseñar una ventana con botones y cajas de texto para una CRUD sencilla.
FLUJO Pasos mentales
- Diseñar interfaz
- Generar o cargar UI
- Conectar señales
- Programar slots
🎯 Mini ejercicio tipo examen
Haz una ventana con botón Guardar y lista de elementos.
⚡ Resumen ultra rápido
- Qt usa señales y slots
- El diseño visual no sustituye la lógica
🛠️ Qué es Qt Designer
Herramienta visual para crear ventanas de aplicaciones Python con PyQt5 o PySide. Genera ficheros .ui (XML) que se cargan o convierten en Python.
Dos formas de usar el .ui:
- Opción 1: Cargar el .ui directamente desde Python → más sencillo.
- Opción 2: Convertir el .ui a .py con
pyuic5→ más control pero más código.
📐 Layouts en Qt Designer
- Vertical Layout → elementos apilados de arriba a abajo.
- Horizontal Layout → elementos en fila de izquierda a derecha.
- Grid Layout → cuadrícula, añade filas y columnas según lo que insertas.
- Form Layout → pensado para formularios (label + campo en pares).
- Spacer (horizontal/vertical) → espacio flexible entre elementos.
🧩 Widgets principales
Botones
- QPushButton → botón normal
- QToolButton → botón que queda pulsado
- QRadioButton → opción única
- QCheckBox → selección múltiple
Entrada y texto
- QLineEdit → campo de texto
- QLabel → etiqueta de texto
- QComboBox → lista desplegable
- QTableWidget → tabla de datos
🐍 Qt Designer — widgets principales
| Widget Qt | Equivalente Swing | Acceso en Python |
|---|---|---|
| QLabel | JLabel | self.labelNombre.setText("hola") |
| QLineEdit | JTextField | self.lineEditNombre.text() |
| QPushButton | JButton | self.btnAceptar.clicked.connect(fn) |
| QComboBox | JComboBox | self.comboBox.currentText() |
| QTableWidget | JTable | self.tableWidget |
| QListWidget | JList | self.listWidget |
Cargar un .ui y conectar señales
# main.py — cargar ventana desde archivo .ui de Qt Designer
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox
from PyQt5.uic import loadUi
class VentanaPrincipal(QWidget):
def __init__(self):
super().__init__()
loadUi("ventana.ui", self) # carga el .ui generado por Qt Designer
# Conectar señales (objectName del widget en Qt Designer)
self.btnAceptar.clicked.connect(self.on_aceptar)
self.btnSalir.clicked.connect(self.on_salir)
# Cargar datos al arrancar (p.ej. ComboBox con 5 nombres)
nombres = ["Ana García", "Luis Pérez", "María López", "Pedro Ruiz", "Sara Martín"]
self.comboBox.addItems(nombres)
# Cuando cambia el combo → mostrar en campo no editable
self.comboBox.currentIndexChanged.connect(self.on_combo_cambio)
def on_combo_cambio(self):
seleccionado = self.comboBox.currentText()
self.lineEditSeleccionado.setText(seleccionado)
self.lineEditSeleccionado.setReadOnly(True)
def on_aceptar(self):
nombre = self.lineEditNombre.text().strip()
if not nombre:
QMessageBox.warning(self, "Error", "Rellena el campo nombre")
return
QMessageBox.information(self, "OK", f"Hola {nombre}")
def on_salir(self):
sys.exit()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = VentanaPrincipal()
w.show()
sys.exit(app.exec_())python
📊 QTableWidget — insertar y borrar filas
# Configurar tabla en __init__
self.tableWidget.setColumnCount(4)
self.tableWidget.setHorizontalHeaderLabels(["DNI", "Nombre", "Apellido", "Edad"])
self.tableWidget.horizontalHeader().setStretchLastSection(True)
self.tableWidget.setEditTriggers(QTableWidget.NoEditTriggers) # no editable
# Insertar fila
def on_insertar(self):
dni = self.lineEditDNI.text().strip()
nombre = self.lineEditNombre.text().strip()
apellido = self.lineEditApellido.text().strip()
edad = self.lineEditEdad.text().strip()
if not all([dni, nombre, apellido, edad]):
QMessageBox.warning(self, "Error", "Rellena todos los campos")
return
# Comprobar DNI duplicado
for row in range(self.tableWidget.rowCount()):
if self.tableWidget.item(row, 0).text() == dni:
QMessageBox.warning(self, "Error", "El DNI ya existe")
return
fila = self.tableWidget.rowCount()
self.tableWidget.insertRow(fila)
self.tableWidget.setItem(fila, 0, QTableWidgetItem(dni))
self.tableWidget.setItem(fila, 1, QTableWidgetItem(nombre))
self.tableWidget.setItem(fila, 2, QTableWidgetItem(apellido))
self.tableWidget.setItem(fila, 3, QTableWidgetItem(edad))
self.on_limpiar()
# Borrar fila seleccionada
def on_borrar(self):
fila = self.tableWidget.currentRow()
if fila == -1:
QMessageBox.warning(self, "Error", "Selecciona una fila")
return
self.tableWidget.removeRow(fila)
# Limpiar campos
def on_limpiar(self):
self.lineEditDNI.clear()
self.lineEditNombre.clear()
self.lineEditApellido.clear()
self.lineEditEdad.clear()python
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Leer una tabla SQLite/MySQL y mostrar filas por consola o en interfaz.
FLUJO Pasos mentales
- Abrir conexión
- Crear cursor
- Ejecutar SQL
- Recorrer resultados
- Cerrar
🎯 Mini ejercicio tipo examen
Consulta todos los alumnos y muestra solo nombre y nota.
⚡ Resumen ultra rápido
- Cursor ejecuta SQL
- fetchall/fetchone recuperan filas
- Hay que cerrar conexión
📦 Instalar el conector
# En el terminal de IntelliJ (o CMD):
# Opción 1 — pymysql (recomendada por el profe, código abierto, 100% Python)
pip install pymysql
# Opción 2 — mysql-connector-python (oficial de Oracle)
pip install mysql-connector-python
# Comprobar que está instalado:
pip list # lista todos los paquetes instaladosbash
🔌 Clase Conexión en Python
# conexion_bd.py
import pymysql # o: import mysql.connector
class ConexionBD:
# Atributos de conexión
HOST = "localhost"
USER = "root"
PASSWORD = "" # vacío por defecto en XAMPP
DATABASE = "mi_base"
def __init__(self):
self.conexion = None
self.cursor = None
def conectar(self):
try:
self.conexion = pymysql.connect(
host = self.HOST,
user = self.USER,
password = self.PASSWORD,
database = self.DATABASE
)
self.cursor = self.conexion.cursor()
print("Conexión establecida")
return self.conexion
except Exception as e:
print(f"Error conexión: {e}")
return None
def desconectar(self):
try:
if self.cursor: self.cursor.close()
if self.conexion: self.conexion.close()
except Exception as e:
print(f"Error cierre: {e}")python
🗄️ Clase GestiónBD en Python
# gestion_bd.py
from conexion_bd import ConexionBD
class GestionBD:
def __init__(self):
self.bd = ConexionBD()
self.bd.conectar()
# ── SELECT: comprobar login ──
def comprobar_login(self, usuario, password):
try:
sql = "SELECT * FROM usuarios WHERE usuario=%s AND password=%s"
self.bd.cursor.execute(sql, (usuario, password))
resultado = self.bd.cursor.fetchone()
return resultado is not None
except Exception as e:
print(f"Error login: {e}")
return False
# ── SELECT: obtener todos los registros ──
def get_alumnos(self):
try:
self.bd.cursor.execute("SELECT * FROM alumnos")
return self.bd.cursor.fetchall() # lista de tuplas
except Exception as e:
print(f"Error: {e}")
return []
# ── INSERT ──
def insertar_alumno(self, nombre, nota):
try:
sql = "INSERT INTO alumnos (nombre, nota) VALUES (%s, %s)"
self.bd.cursor.execute(sql, (nombre, nota))
self.bd.conexion.commit() # ¡sin commit no se guarda!
return True
except Exception as e:
print(f"Error insert: {e}")
return Falsepython
%s (no con ? como en Java). Y para INSERT/UPDATE/DELETE es obligatorio hacer commit() o los cambios no se guardan.🔐 Login con Qt Designer (cargando el .ui)
# main.py — carga el .ui y conecta con la lógica
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox
from PyQt5.uic import loadUi
from gestion_bd import GestionBD
class LoginWindow(QWidget):
def __init__(self):
super().__init__()
loadUi("login.ui", self) # carga el fichero .ui
self.gbd = GestionBD()
# Conectar botones con métodos
# "botonAceptar" es el objectName del botón en Qt Designer
self.botonAceptar.clicked.connect(self.on_entrar)
self.botonSalir.clicked.connect(self.on_salir)
def on_entrar(self):
# lineEditUsuario, lineEditContra = objectName de los campos
usuario = self.lineEditUsuario.text()
contra = self.lineEditContra.text()
if self.gbd.comprobar_login(usuario, contra):
QMessageBox.information(self, "OK", "Bienvenido")
# aquí abres la siguiente ventana
else:
QMessageBox.warning(self, "Error", "Credenciales incorrectas")
def on_salir(self):
sys.exit()
# ── Punto de entrada ──
if __name__ == "__main__":
app = QApplication(sys.argv)
ventana = LoginWindow()
ventana.show()
sys.exit(app.exec_())python
objectName en Qt Designer es exactamente el que usas en Python (self.botonAceptar, self.lineEditUsuario...).📋 Resumen — Diferencias entre las 3 tecnologías
// ──────────── SWING ────────────
// Diseño: WindowBuilder (plugin Eclipse) — arrastrar/soltar
// Archivo: LoginFrame.java (solo Java, sin XML separado)
// Evento: btnEntrar.addActionListener(this) → actionPerformed()
// Ventana: new LoginFrame().setVisible(true)
// ──────────── JAVAFX ────────────
// Diseño: SceneBuilder (app separada) — genera login.fxml
// Archivo: login.fxml + LoginController.java
// Evento: On Action en SceneBuilder = @FXML public void onEntrar()
// Ventana: FXMLLoader + Stage → stage.setScene(scene); stage.show()
// ──────────── PYTHON / Qt ────────────
// Diseño: Qt Designer — genera login.ui
// Archivo: login.ui (XML) + main.py
// Evento: self.botonAceptar.clicked.connect(self.on_entrar)
// Ventana: loadUi("login.ui", self) → ventana.show()resumen
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Formulario de alta + tabla + botón de guardar o buscar.
FLUJO Pasos mentales
- Diseñar mínima interfaz
- Conectar evento
- Leer datos
- Actualizar componente visual
🎯 Mini ejercicio tipo examen
Escribe el plan de una app para gestionar tareas con una tabla y botones Añadir/Borrar.
⚡ Resumen ultra rápido
- Primero que funcione el evento
- Luego conectar BD
- Luego mejorar interfaz
📋 Patrón del examen
| Pregunta | Tecnología | Lo que siempre piden |
|---|---|---|
| 1 | Swing | Ventana con tabla + INSERTAR (sin DNI duplicado) + BORRAR fila + LIMPIAR + SALIR |
| 2 | Python/Qt | Solo el archivo .ui de la ventana mostrada (sin código) |
| 3 | JavaFX | Misma ventana que Swing: INSERTAR (sin duplicado) + SALIR |
🖥️ EXAMEN — Swing: Ventana con tabla CRUD (sin BD)
Campos: DNI, Nombre, Apellido, Edad. Tabla con esas columnas. INSERTAR sin duplicar DNI, BORRAR fila seleccionada, LIMPIAR campos, SALIR.
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
public class VentanaPrincipal extends JFrame implements ActionListener {
// Campos de texto
private JTextField txtDni, txtNombre, txtApellido, txtEdad;
// Tabla
private JTable tabla;
private DefaultTableModel modelo;
// Botones
private JButton btnInsertar, btnBorrar, btnLimpiar, btnSalir;
public VentanaPrincipal() {
setTitle("Gestión de Personas");
setSize(700, 500);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
initComponentes();
initListeners();
}
private void initComponentes() {
setLayout(new BorderLayout(10, 10));
// ─── Panel Norte: campos de entrada ───
JPanel pNorte = new JPanel(new GridLayout(4, 2, 5, 5));
pNorte.setBorder(BorderFactory.createTitledBorder("Datos"));
txtDni = new JTextField();
txtNombre = new JTextField();
txtApellido = new JTextField();
txtEdad = new JTextField();
pNorte.add(new JLabel("DNI:")); pNorte.add(txtDni);
pNorte.add(new JLabel("Nombre:")); pNorte.add(txtNombre);
pNorte.add(new JLabel("Apellido:")); pNorte.add(txtApellido);
pNorte.add(new JLabel("Edad:")); pNorte.add(txtEdad);
// ─── Panel Centro: tabla ───
String[] columnas = {"DNI", "Nombre", "Apellido", "Edad"};
modelo = new DefaultTableModel(columnas, 0) {
@Override public boolean isCellEditable(int r, int c) { return false; }
};
tabla = new JTable(modelo);
tabla.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// ─── Panel Sur: botones ───
btnInsertar = new JButton("INSERTAR");
btnBorrar = new JButton("BORRAR");
btnLimpiar = new JButton("LIMPIAR");
btnSalir = new JButton("SALIR");
JPanel pSur = new JPanel(new FlowLayout());
pSur.add(btnInsertar); pSur.add(btnBorrar);
pSur.add(btnLimpiar); pSur.add(btnSalir);
add(pNorte, BorderLayout.NORTH);
add(new JScrollPane(tabla), BorderLayout.CENTER);
add(pSur, BorderLayout.SOUTH);
}
private void initListeners() {
btnInsertar.addActionListener(this);
btnBorrar.addActionListener(this);
btnLimpiar.addActionListener(this);
btnSalir.addActionListener(this);
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == btnInsertar) insertar();
else if (e.getSource() == btnBorrar) borrar();
else if (e.getSource() == btnLimpiar) limpiar();
else if (e.getSource() == btnSalir) System.exit(0);
}
private void insertar() {
String dni = txtDni.getText().trim();
String nombre = txtNombre.getText().trim();
String apellido = txtApellido.getText().trim();
String edad = txtEdad.getText().trim();
// Validar: todos los campos rellenos
if (dni.isEmpty() || nombre.isEmpty() || apellido.isEmpty() || edad.isEmpty()) {
JOptionPane.showMessageDialog(this, "Rellena todos los campos", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
// Validar: DNI no duplicado — recorre todas las filas
for (int i = 0; i < modelo.getRowCount(); i++) {
if (modelo.getValueAt(i, 0).toString().equalsIgnoreCase(dni)) {
JOptionPane.showMessageDialog(this, "El DNI ya existe", "Error", JOptionPane.ERROR_MESSAGE);
return;
}
}
// Insertar fila en la tabla
modelo.addRow(new Object[]{dni, nombre, apellido, edad});
limpiar();
}
private void borrar() {
int fila = tabla.getSelectedRow();
if (fila == -1) {
JOptionPane.showMessageDialog(this, "Selecciona una fila");
return;
}
modelo.removeRow(fila);
}
private void limpiar() {
txtDni.setText(""); txtNombre.setText("");
txtApellido.setText(""); txtEdad.setText("");
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new VentanaPrincipal().setVisible(true));
}
}java
🌟 EXAMEN — JavaFX: Misma ventana con INSERTAR + SALIR
// controller/PrincipalController.java
public class PrincipalController {
@FXML private TextField tfDni, tfNombre, tfApellido, tfEdad;
@FXML private TableView<Persona> tabla;
@FXML private TableColumn<Persona,String> colDni, colNombre, colApellido, colEdad;
private ObservableList<Persona> datos = FXCollections.observableArrayList();
@FXML
public void initialize() {
// Vincular columnas con los getter de Persona
colDni.setCellValueFactory(new PropertyValueFactory<>("dni"));
colNombre.setCellValueFactory(new PropertyValueFactory<>("nombre"));
colApellido.setCellValueFactory(new PropertyValueFactory<>("apellido"));
colEdad.setCellValueFactory(new PropertyValueFactory<>("edad"));
tabla.setItems(datos);
}
@FXML
public void onInsertar(ActionEvent e) {
String dni = tfDni.getText().trim();
String nombre = tfNombre.getText().trim();
String apellido = tfApellido.getText().trim();
String edad = tfEdad.getText().trim();
if (dni.isEmpty() || nombre.isEmpty() || apellido.isEmpty() || edad.isEmpty()) {
mostrarAlerta("Rellena todos los campos"); return;
}
// Comprobar DNI duplicado
boolean existe = datos.stream().anyMatch(p -> p.getDni().equalsIgnoreCase(dni));
if (existe) { mostrarAlerta("El DNI ya existe"); return; }
datos.add(new Persona(dni, nombre, apellido, edad));
tfDni.clear(); tfNombre.clear(); tfApellido.clear(); tfEdad.clear();
}
@FXML
public void onSalir(ActionEvent e) {
Platform.exit();
}
private void mostrarAlerta(String msg) {
Alert alert = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
alert.showAndWait();
}
}
// model/Persona.java — necesita getters para PropertyValueFactory
public class Persona {
private String dni, nombre, apellido, edad;
public Persona(String dni, String nombre, String apellido, String edad) {
this.dni=dni; this.nombre=nombre; this.apellido=apellido; this.edad=edad;
}
public String getDni() { return dni; }
public String getNombre() { return nombre; }
public String getApellido() { return apellido; }
public String getEdad() { return edad; }
}java
🐍 EXAMEN anterior — Swing: ComboBox + Login estático
ComboBox cargado al arrancar (5 nombres)
public class VentanaCombo extends JFrame implements ActionListener {
private JComboBox<String> cbNombres;
private JTextField txtSeleccionado;
public VentanaCombo() {
setTitle("Seleccionar nombre"); setSize(400, 200);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new FlowLayout());
// 5 nombres cargados al arrancar
String[] nombres = {"Ana García", "Luis Pérez", "María López", "Pedro Ruiz", "Sara Martín"};
cbNombres = new JComboBox<>(nombres);
// Campo no editable donde aparece la selección
txtSeleccionado = new JTextField(20);
txtSeleccionado.setEditable(false);
cbNombres.addActionListener(this);
add(new JLabel("Elige un nombre:")); add(cbNombres);
add(new JLabel("Seleccionado:")); add(txtSeleccionado);
}
@Override
public void actionPerformed(ActionEvent e) {
// Al elegir en el combo, muestra el nombre en el campo
txtSeleccionado.setText((String) cbNombres.getSelectedItem());
}
}
// ─── Login estático ───
public class VentanaLogin extends JFrame implements ActionListener {
private static final String USUARIO = "admin";
private static final String PASSWORD = "1234";
private JTextField txtUsuario;
private JPasswordField pfPassword;
private JButton btnEntrar, btnSalir;
public VentanaLogin() {
setTitle("Login"); setSize(350, 200);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new GridLayout(4, 2, 5, 5));
txtUsuario = new JTextField();
pfPassword = new JPasswordField();
btnEntrar = new JButton("Entrar");
btnSalir = new JButton("Salir");
add(new JLabel("Usuario:")); add(txtUsuario);
add(new JLabel("Contraseña:")); add(pfPassword);
add(btnEntrar); add(btnSalir);
btnEntrar.addActionListener(this);
btnSalir.addActionListener(this);
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == btnSalir) { System.exit(0); return; }
String user = txtUsuario.getText().trim();
String pass = new String(pfPassword.getPassword());
if (user.equals(USUARIO) && pass.equals(PASSWORD)) {
JOptionPane.showMessageDialog(this, "¡Bienvenido a la aplicación!");
} else {
JOptionPane.showMessageDialog(this, "Datos incorrectos", "Error", JOptionPane.ERROR_MESSAGE);
}
}
}java
🐍 EXAMEN — Python: Ventana login con Qt Designer + código
# main.py — carga el .ui y gestiona el login
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox
from PyQt5.uic import loadUi
USUARIO_OK = "admin"
PASSWORD_OK = "1234"
class LoginWindow(QWidget):
def __init__(self):
super().__init__()
loadUi("login.ui", self) # nombre del .ui generado con Qt Designer
# Conectar botones (los objectName deben coincidir con los del .ui)
self.btnEntrar.clicked.connect(self.on_entrar)
self.btnSalir.clicked.connect(self.on_salir)
def on_entrar(self):
user = self.lineEditUsuario.text().strip()
pwd = self.lineEditPassword.text().strip()
if user == USUARIO_OK and pwd == PASSWORD_OK:
QMessageBox.information(self, "OK", "¡Bienvenido a la aplicación!")
else:
QMessageBox.warning(self, "Error", "Datos incorrectos")
def on_salir(self):
sys.exit()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = LoginWindow()
w.show()
sys.exit(app.exec_())python
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Comparar FCFS, SJF y Round Robin con varios procesos y tiempos de llegada.
FLUJO Pasos mentales
- Identificar procesos
- Ordenarlos según algoritmo
- Calcular espera y retorno
- Comparar resultado
🎯 Mini ejercicio tipo examen
Resuelve una tabla simple de planificación con tres procesos y FCFS.
⚡ Resumen ultra rápido
- Proceso = programa ejecutándose
- La planificación reparte CPU
- Cada algoritmo favorece cosas distintas
🧠 Conceptos clave (salen en el test)
| Concepto | Definición |
|---|---|
| Proceso | Un programa en ejecución. Cuando le das al play, creas un proceso. Tiene su propio espacio de memoria. |
| Hilo (thread) | Sub-proceso dentro de un proceso. Comparte memoria con el proceso padre. No puede existir sin un proceso. |
| Concurrencia | Varios hilos/procesos progresan alternando el uso del CPU (un solo núcleo simula ejecución paralela). |
| Paralelismo | Varios hilos se ejecutan AL MISMO TIEMPO en núcleos distintos del CPU. |
| Planificador | Parte del SO que decide qué proceso/hilo ejecuta el CPU en cada momento. |
| Sección crítica | Trozo de código que accede a un recurso compartido y que solo 1 hilo puede ejecutar a la vez. |
📊 Los 4 Algoritmos de Planificación (salen en papel)
| Algoritmo | Funciona así | Expulsivo | Empate |
|---|---|---|---|
| FCFS (First Come First Served) | El primero que llega, primero en ejecutar. Cola de supermercado. | No | – |
| SJF (Shortest Job First) | El proceso más corto ejecuta primero. | No | FCFS |
| SRTF (Shortest Remaining Time First) | Como SJF pero si llega uno más corto, el actual es expulsado. | Sí | FCFS |
| Round Robin | Cada proceso recibe un "quantum" de tiempo. Cuando termina, pasa el siguiente. | Sí | FCFS |
Ejemplo resuelto — FCFS
Procesos: P1 (llega t=0, dura 4), P2 (llega t=1, dura 3), P3 (llega t=2, dura 2)
// FCFS — no expulsivo, orden de llegada
// P1 llega primero → ejecuta completo, luego P2, luego P3
//
// Gantt: | P1 | P1 | P1 | P1 | P2 | P2 | P2 | P3 | P3 |
// t=0 t=1 t=2 t=3 t=4 t=5 t=6 t=7 t=8 t=9
//
// Tiempo de finalización: P1=4, P2=7, P3=9
// Tiempo de espera: P1=0, P2=4-1=3, P3=7-2=5
// Espera promedio: (0+3+5)/3 = 2.67 unidadesplanificación
Ejemplo resuelto — SRTF (expulsivo)
// SRTF — expulsivo. Si llega uno más corto, expulsa al actual.
// P1(t=0, dur=4) P2(t=1, dur=3) P3(t=2, dur=2)
//
// t=0: llega P1 (remaining=4) → ejecuta P1
// t=1: llega P2 (remaining=3) → P1 tiene 3 restantes, P2 tiene 3 → EMPATE → sigue P1
// t=2: llega P3 (remaining=2) → P3 es más corto → expulsa a P1 → ejecuta P3
// t=4: P3 termina → P2(rem=3) vs P1(rem=2) → P1 es más corto → ejecuta P1
// t=6: P1 termina → ejecuta P2
// t=9: P2 termina
//
// Gantt: |P1|P1|P3|P3|P1|P1|P2|P2|P2|
// t=0 t=1 t=2 t=3 t=4 t=5 t=6 t=7 t=8 t=9planificación
⚠️ Lo que entra en el examen del tema 1
- Test teórico: definiciones de proceso, hilo, concurrencia, paralelismo, estados de un proceso.
- Ejercicio en papel: tabla de planificación con diagrama de Gantt, tiempo de espera y tiempo promedio.
- No entra: código Java de procesos (ProcessBuilder, Runtime). Solo para entender el concepto.
🔄 Estados de un proceso (test)
// NUEVO → le das al play. El proceso se crea.
// PREPARADO → está en la cola, esperando que el planificador le asigne CPU.
// EN EJECUCIÓN → el CPU está ejecutando sus instrucciones.
// BLOQUEADO → espera E/S (leer fichero, entrada teclado...). Vuelve a PREPARADO cuando termina.
// TERMINADO → el proceso ha acabado. Libera recursos.
//
// [NUEVO] → [PREPARADO] ⇄ [EN EJECUCIÓN] → [TERMINADO]
// ↑ ↓
// └──── [BLOQUEADO] ←────teoría
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Un hilo cuenta números y otro imprime letras al mismo tiempo.
FLUJO Pasos mentales
- Crear clase Thread o Runnable
- Implementar
run() - Llamar a
start() - Observar concurrencia
🎯 Mini ejercicio tipo examen
Crea dos hilos: uno imprime pares y otro impares.
⚡ Resumen ultra rápido
start()crea el hilo realrun()contiene el trabajo- Runnable da más flexibilidad
🧵 Las dos formas de crear hilos
① implements Runnable (recomendada)
Implementas la interfaz. Puedes seguir heredando de otra clase. Se pueden lanzar varios hilos sobre un mismo objeto (atributos compartidos).
② extends Thread
Hereda de Thread. Ya no puedes heredar de otra clase. Cada hilo es un objeto diferente (atributos no compartidos).
Runnable. Si quieres compartir atributos entre todos los hilos → Runnable. Si no → cualquiera.① Hilos con implements Runnable
// ─── Clase Galgo (el hilo) ───
public class Galgo implements Runnable {
private String nombre;
private int tiempoCarrera; // tiempo propio de cada galgo
// Variable ESTÁTICA = compartida por TODOS los galgos
private static int tiempoTotalCarrera = 0;
private static int posicion = 1;
public Galgo(String nombre, int tiempoCarrera) {
this.nombre = nombre;
this.tiempoCarrera = tiempoCarrera;
}
// El método run() es el "main" del hilo. Se ejecuta al llamar start().
@Override
public void run() {
System.out.println(nombre + " empieza a correr...");
try {
Thread.sleep(tiempoCarrera); // simula tiempo corriendo
} catch (InterruptedException e) {
System.out.println(nombre + " fue interrumpido");
}
System.out.println(nombre + " llega en posición " + posicion++);
}
}
// ─── Clase Principal (el main) ───
public class Principal {
public static void main(String[] args) {
Random rnd = new Random();
String[] nombres = {"Rayo", "Trueno", "Furia", "Viento"};
// Con Runnable: envuelvo el objeto en Thread para lanzarlo
Thread[] hilos = new Thread[nombres.length];
for (int i = 0; i < nombres.length; i++) {
Galgo galgo = new Galgo(nombres[i], rnd.nextInt(2000) + 1000);
hilos[i] = new Thread(galgo); // envuelvo el Runnable
hilos[i].start(); // IMPORTANTE: start() no run()
}
// join() espera a que todos terminen antes de continuar
for (Thread h : hilos) {
try { h.join(); }
catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.println("¡Carrera terminada! Todos los galgos han llegado.");
}
}java
② Hilos con extends Thread
// ─── Clase Raton — hereda de Thread directamente ───
public class Raton extends Thread {
private String nombre;
private int tiempoComiendo;
public Raton(String nombre, int tiempoComiendo) {
this.nombre = nombre;
this.tiempoComiendo = tiempoComiendo;
}
@Override
public void run() {
System.out.printf("%s empieza a comer%n", nombre);
try {
Thread.sleep(tiempoComiendo);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.printf("%s ha terminado de comer%n", nombre);
}
}
// ─── Main ───
public static void main(String[] args) throws InterruptedException {
// Con Thread: el propio objeto es el hilo
Raton r1 = new Raton("Mickey", 1500);
Raton r2 = new Raton("Stuart", 2000);
Raton r3 = new Raton("Ratatouille", 800);
r1.start(); // ← start(), NO run()
r2.start(); // run() solo llamaría el método secuencialmente
r3.start();
r1.join(); // espera a que r1 termine
r2.join();
r3.join();
System.out.println("Todos los ratones han comido.");
}java
run() en vez de start(). Si llamas a run(), el método se ejecuta secuencialmente en el hilo principal, NO crea ningún hilo nuevo.📊 Variables estáticas — compartidas entre todos los hilos
public class Donante implements Runnable {
private String nombre;
private double cantidad; // PROPIA de cada donante
// STATIC = hay UNA SOLA copia compartida por todos los donantes
private static double totalRecaudado = 0.0;
public Donante(String nombre, double cantidad) {
this.nombre = nombre;
this.cantidad = cantidad;
}
@Override
public void run() {
System.out.println(nombre + " dona " + cantidad + "€");
try { Thread.sleep(500); }
catch (InterruptedException e) { }
totalRecaudado += cantidad; // afecta a TODOS
System.out.println(nombre + " → Total acumulado: " + totalRecaudado + "€");
}
public static double getTotalRecaudado() { return totalRecaudado; }
}
// En el main:
public static void main(String[] args) throws InterruptedException {
ArrayList<Thread> hilos = new ArrayList<>();
String[] nombres = {"Ana", "Luis", "María", "Pedro"};
double[] cantidades = {100, 250, 75, 300};
for (int i = 0; i < nombres.length; i++) {
Thread h = new Thread(new Donante(nombres[i], cantidades[i]));
hilos.add(h);
h.start();
}
for (Thread h : hilos) h.join(); // espera a todos antes de mostrar el total
System.out.println("=== TOTAL DONADO: " + Donante.getTotalRecaudado() + "€ ===");
}java
🧪 En práctica — cómo pensarlo
join().CASO REAL Ejemplo de uso
Crear una lista de hilos que procesan partes distintas y esperar al final a que todos acaben.
FLUJO Pasos mentales
- Guardar hilos en ArrayList
- Lanzarlos con
start() - Recorrer lista con
join() - Continuar al final
🎯 Mini ejercicio tipo examen
Lanza cinco hilos y muestra “fin” solo cuando todos hayan terminado.
⚡ Resumen ultra rápido
- ArrayList organiza hilos
- join espera
- Sin join el main puede acabar antes
🔗 ¿Para qué sirve join()?
join() detiene el hilo que lo llama hasta que el hilo llamado termine su ejecución. Se usa para esperar a que todos los hilos terminen ANTES de mostrar resultados totales.
// Sin join() → peligroso: puedes mostrar el total antes de que terminen todos
hilo1.start();
hilo2.start();
hilo3.start();
System.out.println("Total: " + total); // ← se ejecuta ANTES de que terminen los hilos!
// Con join() → correcto: espera a TODOS antes de mostrar el total
hilo1.start();
hilo2.start();
hilo3.start();
hilo1.join(); // espera a hilo1
hilo2.join(); // espera a hilo2
hilo3.join(); // espera a hilo3
System.out.println("Total: " + total); // ← ahora sí es el total realjava
📋 ArrayList de hilos (cuando no sabes cuántos habrá)
Cuando el número de hilos es dinámico (lo decide el usuario), usa un ArrayList para guardarlos y luego hacer join() a todos.
public static void main(String[] args) throws InterruptedException {
Scanner sc = new Scanner(System.in);
System.out.print("¿Cuántos clientes entran hoy? ");
int numClientes = sc.nextInt();
// No puedo escribir 'cliente1.join(), cliente2.join()...' porque no sé cuántos hay
ArrayList<Thread> hilos = new ArrayList<>();
// Fase 1: crear y arrancar todos los hilos
for (int i = 1; i <= numClientes; i++) {
Thread hilo = new Thread(new Supermercado(i));
hilos.add(hilo); // guardo referencia para el join
hilo.start();
}
// Fase 2: esperar a que terminen TODOS
for (Thread h : hilos) {
h.join(); // espera a cada uno
}
// Fase 3: mostrar resultados SOLO cuando todos han terminado
System.out.println("=== CAJA 1: " + Supermercado.caja1 + "€");
System.out.println("=== CAJA 2: " + Supermercado.caja2 + "€");
System.out.println("=== TOTAL: " + (Supermercado.caja1 + Supermercado.caja2) + "€");
}java
🏗️ Estructura completa recomendada — Supermercado con 3 cajas
// ─── Supermercado.java ─── (el hilo)
public class Supermercado implements Runnable {
private int idCliente;
// Variables estáticas: recaudación de cada caja (compartidas)
static double caja1 = 0, caja2 = 0, caja3 = 0;
public Supermercado(int id) { this.idCliente = id; }
@Override
public void run() {
Random rnd = new Random();
int cajaElegida = rnd.nextInt(3) + 1; // caja 1, 2 o 3 aleatoria
double compra = rnd.nextDouble() * 100;
System.out.printf("Cliente %d va a la caja %d (%.2f€)%n", idCliente, cajaElegida, compra);
try { Thread.sleep(500); } // simula tiempo en caja
catch (InterruptedException e) {}
switch (cajaElegida) {
case 1: caja1 += compra; break;
case 2: caja2 += compra; break;
case 3: caja3 += compra; break;
}
System.out.printf("Cliente %d sale de la caja %d%n", idCliente, cajaElegida);
}
}java
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Baño con 2 plazas o impresora con 1 turno; los hilos piden permiso antes de entrar.
FLUJO Pasos mentales
- Crear semáforo
- Acquire antes del recurso
- Usar recurso
- Release al salir
🎯 Mini ejercicio tipo examen
Modela un parking con 3 plazas y 10 coches usando semáforos.
⚡ Resumen ultra rápido
- Acquire entra
- Release libera
- Sirve para controlar concurrencia
🚦 ¿Qué es un semáforo?
Un semáforo controla cuántos hilos pueden acceder simultáneamente a una sección crítica. Es como un torniquete con N permisos.
- acquire() → pide un permiso. Si hay disponible, pasa. Si no, espera bloqueado.
- release() → devuelve un permiso. Desbloquea al siguiente hilo en espera.
- new Semaphore(N) → semáforo con N permisos simultáneos.
- new Semaphore(1) → exclusión mutua (solo 1 hilo a la vez, igual que un mutex).
🏪 Ejemplo completo — Supermercado con semáforos
import java.util.concurrent.Semaphore;
import java.util.Random;
import java.util.ArrayList;
public class Supermercado implements Runnable {
private int idCliente;
// Semáforo de la tienda: máximo 15 personas dentro a la vez
static Semaphore tienda = new Semaphore(15);
// Semáforos de cada caja: solo 1 cliente a la vez en cada caja
static Semaphore caja1 = new Semaphore(1);
static Semaphore caja2 = new Semaphore(1);
static Semaphore caja3 = new Semaphore(1);
// Recaudación de cada caja (estática = compartida)
static double recCaja1 = 0, recCaja2 = 0, recCaja3 = 0;
public Supermercado(int id) { this.idCliente = id; }
@Override
public void run() {
Random rnd = new Random();
try {
// 1. Entrar a la tienda (máx 15 a la vez)
tienda.acquire();
System.out.println("Cliente " + idCliente + " entra a la tienda");
Thread.sleep(1000); // simula tiempo comprando
// 2. Elegir caja aleatoriamente (1, 2 o 3)
int cajaNum = rnd.nextInt(3) + 1;
double compra = rnd.nextDouble() * 100;
// 3. Entrar en la caja elegida (solo 1 a la vez)
switch (cajaNum) {
case 1:
caja1.acquire();
System.out.printf("Cliente %d paga %.2f€ en caja 1%n", idCliente, compra);
Thread.sleep(500);
recCaja1 += compra;
caja1.release();
break;
case 2:
caja2.acquire();
System.out.printf("Cliente %d paga %.2f€ en caja 2%n", idCliente, compra);
Thread.sleep(500);
recCaja2 += compra;
caja2.release();
break;
case 3:
caja3.acquire();
System.out.printf("Cliente %d paga %.2f€ en caja 3%n", idCliente, compra);
Thread.sleep(500);
recCaja3 += compra;
caja3.release();
break;
}
System.out.println("Cliente " + idCliente + " sale de la tienda");
tienda.release(); // libera permiso de la tienda
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// ─── Main ───
public static void main(String[] args) throws InterruptedException {
int numClientes = 30;
ArrayList<Thread> hilos = new ArrayList<>();
for (int i = 1; i <= numClientes; i++) {
Thread h = new Thread(new Supermercado(i));
hilos.add(h);
h.start();
}
for (Thread h : hilos) h.join();
System.out.printf("Caja 1: %.2f€ | Caja 2: %.2f€ | Caja 3: %.2f€%n",
Supermercado.recCaja1, Supermercado.recCaja2, Supermercado.recCaja3);
System.out.printf("TOTAL: %.2f€%n",
Supermercado.recCaja1 + Supermercado.recCaja2 + Supermercado.recCaja3);
}java
🧪 En práctica — cómo pensarlo
synchronized proteges secciones críticas para que dos hilos no toquen lo mismo a la vez.CASO REAL Ejemplo de uso
Cuenta bancaria compartida donde varios hilos ingresan o retiran dinero.
FLUJO Pasos mentales
- Detectar dato compartido
- Encerrar acceso en método synchronized
- Probar con varios hilos
🎯 Mini ejercicio tipo examen
Haz una clase Contador con método incrementar seguro para hilos.
⚡ Resumen ultra rápido
- Sección crítica = zona peligrosa
- synchronized evita accesos simultáneos
- Protege datos compartidos
🔒 ¿Qué es un monitor?
Un monitor es el mecanismo de Java para asegurar que solo un hilo puede ejecutar un método o bloque a la vez. Se implementa con la palabra clave synchronized.
synchronized en método
Bloquea el método entero. Solo 1 hilo puede ejecutarlo a la vez.
public synchronized void ingresar(double cantidad) {
saldo += cantidad;
}java
synchronized en bloque
Más granular. Solo bloquea el trozo crítico, no todo el método.
public void operar() {
// código libre (no bloqueado)
synchronized (this) {
saldo += cantidad; // solo esto bloqueado
}
// más código libre
}java
🏦 Ejemplo completo — Cuenta bancaria compartida
// ─── Cuenta.java ─── (recurso compartido)
public class Cuenta {
private String iban;
private static double saldo = 1000.0; // saldo compartido
public Cuenta(String iban) { this.iban = iban; }
// synchronized: solo 1 hilo puede ingresar a la vez
public synchronized void ingresar(String usuario, double cantidad)
throws InterruptedException {
System.out.println(usuario + " empieza a ingresar " + cantidad + "€");
Thread.sleep(500);
saldo += cantidad;
System.out.printf("%s ingresa %.2f€ → Saldo: %.2f€%n", usuario, cantidad, saldo);
}
// synchronized: solo 1 hilo puede retirar a la vez (para evitar saldo negativo)
public synchronized void retirar(String usuario, double cantidad)
throws InterruptedException {
if (cantidad > saldo) {
System.out.println(usuario + ": saldo insuficiente");
return;
}
System.out.println(usuario + " empieza a retirar " + cantidad + "€");
Thread.sleep(500);
saldo -= cantidad;
System.out.printf("%s retira %.2f€ → Saldo: %.2f€%n", usuario, cantidad, saldo);
}
public static double getSaldo() { return saldo; }
}
// ─── Usuario.java ─── (el hilo)
public class Usuario implements Runnable {
private String dni, nombre;
private double cantidad;
private boolean ingresa; // true=ingresa, false=retira
private Cuenta cuenta; // objeto compartido entre todos los usuarios
public Usuario(String dni, String nombre, double cantidad,
boolean ingresa, Cuenta cuenta) {
this.dni = dni;
this.nombre = nombre;
this.cantidad = cantidad;
this.ingresa = ingresa;
this.cuenta = cuenta;
}
@Override
public void run() {
try {
if (ingresa) cuenta.ingresar(nombre, cantidad);
else cuenta.retirar(nombre, cantidad);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
// ─── Main ───
public static void main(String[] args) throws InterruptedException {
Cuenta cuentaCompartida = new Cuenta("ES12345678");
// TODOS comparten el mismo objeto cuenta
Thread u1 = new Thread(new Usuario("11A", "Ana", 200, true, cuentaCompartida));
Thread u2 = new Thread(new Usuario("22B", "Luis", 500, false, cuentaCompartida));
Thread u3 = new Thread(new Usuario("33C", "María", 300, true, cuentaCompartida));
u1.start(); u2.start(); u3.start();
u1.join(); u2.join(); u3.join();
System.out.printf("Saldo final: %.2f€%n", Cuenta.getSaldo());
}java
⚙️ wait(), notify() y notifyAll()
Métodos para coordinar hilos dentro de un bloque synchronized. Solo se usan dentro de synchronized.
| Método | Qué hace | Cuándo usarlo |
|---|---|---|
wait() | El hilo actual SE PAUSA y libera el monitor | Cuando el hilo no puede continuar y tiene que esperar que otro lo notifique |
notify() | Despierta al PRIMERO de la cola de espera | Cuando solo hay 2 hilos alternando (productor-consumidor simple) |
notifyAll() | Despierta a TODOS los hilos en espera | Cuando hay varios hilos esperando y quieres que todos compitan. Recomendado. |
Ejemplo Productor-Consumidor
// ─── Monitor.java ─── (recurso compartido con la lógica)
public class Monitor {
private int elemento = 0;
private boolean hayElemento = false;
// Solo puede insertar si NO hay elemento ya (hayElemento=false)
public synchronized void insertar(int valor) throws InterruptedException {
while (hayElemento) {
wait(); // espera a que el consumidor lo quite
}
elemento = valor;
hayElemento = true;
System.out.println("PRODUCTOR insertó: " + valor);
notifyAll(); // avisa al consumidor
}
// Solo puede extraer si HAY elemento (hayElemento=true)
public synchronized int extraer() throws InterruptedException {
while (!hayElemento) {
wait(); // espera a que el productor lo ponga
}
int valor = elemento;
hayElemento = false;
System.out.println("CONSUMIDOR extrajo: " + valor);
notifyAll(); // avisa al productor
return valor;
}
}
// ─── Productor.java ───
public class Productor implements Runnable {
private Monitor monitor;
public Productor(Monitor m) { this.monitor = m; }
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
monitor.insertar(i);
Thread.sleep(300);
} catch (InterruptedException e) {}
}
}
}
// ─── Consumidor.java ───
public class Consumidor implements Runnable {
private Monitor monitor;
public Consumidor(Monitor m) { this.monitor = m; }
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
monitor.extraer();
Thread.sleep(500);
} catch (InterruptedException e) {}
}
}
}
// ─── Main ───
public static void main(String[] args) throws InterruptedException {
Monitor m = new Monitor(); // UN SOLO monitor compartido
Thread prod = new Thread(new Productor(m));
Thread cons = new Thread(new Consumidor(m));
prod.start(); cons.start();
prod.join(); cons.join();
}java
while(!condición) wait() en vez de if(!condición) wait(). El while protege contra "falsas activaciones" (spurious wakeups).wait() antes de hacer el notifyAll() si el otro hilo ya está esperando, porque ambos quedarán bloqueados (deadlock).🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Chat simple donde el cliente envía texto y el servidor responde “OK”.
FLUJO Pasos mentales
- Crear ServerSocket
- Aceptar cliente
- Abrir flujos
- Enviar/recibir
🎯 Mini ejercicio tipo examen
Haz un cliente que mande un nombre y el servidor responda “Hola nombre”.
⚡ Resumen ultra rápido
- TCP = conexión
- ServerSocket espera
- Socket comunica
- Streams mueven datos
🌐 Protocolos: TCP vs UDP
| TCP | UDP | |
|---|---|---|
| Conexión | Orientado a conexión (handshake) | Sin conexión |
| Fiabilidad | Garantiza que los datos llegan y en orden | No garantiza nada |
| Velocidad | Más lento | Más rápido |
| Uso típico | Web, email, transferencia de ficheros | Streaming, videojuegos, DNS, DHCP |
| Clases Java | ServerSocket, Socket | DatagramSocket, DatagramPacket |
📡 Servidor TCP — Estructura básica
import java.net.*;
import java.io.*;
public class Servidor {
private ServerSocket serverSocket;
private Socket socket;
private DataInputStream entrada;
private DataOutputStream salida;
public void arrancar(int puerto) {
try {
// 1. Crear el ServerSocket en el puerto indicado
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("localhost", puerto));
System.out.println("Servidor esperando en puerto " + puerto + "...");
// 2. Esperar conexión del cliente (BLOQUEANTE hasta que conecte)
socket = serverSocket.accept();
System.out.println("Cliente conectado: " + socket.getInetAddress());
// 3. Abrir flujos de entrada y salida
entrada = new DataInputStream(socket.getInputStream());
salida = new DataOutputStream(socket.getOutputStream());
// 4. Recibir un número del cliente y devolver su doble
int numero = entrada.readInt();
System.out.println("Recibido: " + numero);
salida.writeInt(numero * 2);
salida.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
parar();
}
}
public void parar() {
try {
if (entrada != null) entrada.close();
if (salida != null) salida.close();
if (socket != null) socket.close();
if (serverSocket!= null) serverSocket.close();
} catch (IOException e) { e.printStackTrace(); }
}
public static void main(String[] args) {
new Servidor().arrancar(5000);
}
}java
💻 Cliente TCP — Estructura básica
import java.net.*;
import java.io.*;
public class Cliente {
private Socket socket;
private DataInputStream entrada;
private DataOutputStream salida;
public void conectar(String host, int puerto) {
try {
// 1. Conectar al servidor
socket = new Socket(host, puerto);
System.out.println("Conectado al servidor");
// 2. Abrir flujos
entrada = new DataInputStream(socket.getInputStream());
salida = new DataOutputStream(socket.getOutputStream());
// 3. Enviar un número y recibir el resultado
Scanner sc = new Scanner(System.in);
System.out.print("Introduce un número: ");
int num = sc.nextInt();
salida.writeInt(num); // envía al servidor
salida.flush();
int respuesta = entrada.readInt(); // recibe del servidor
System.out.println("El doble es: " + respuesta);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (entrada != null) entrada.close();
if (salida != null) salida.close();
if (socket != null) socket.close();
} catch (IOException e) { e.printStackTrace(); }
}
}
public static void main(String[] args) {
new Cliente().conectar("localhost", 5000);
}
}java
📨 Enviar y recibir Strings (PrintWriter + BufferedReader)
// En el servidor:
BufferedReader entrada = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter salida = new PrintWriter(socket.getOutputStream(), true); // true = auto-flush
String mensajeRecibido = entrada.readLine(); // lee hasta '\n'
salida.println("Respuesta del servidor: OK"); // envía con '\n' al final
// En el cliente (igual):
PrintWriter salida = new PrintWriter(socket.getOutputStream(), true);
BufferedReader entrada = new BufferedReader(new InputStreamReader(socket.getInputStream()));
salida.println("Hola servidor");
String respuesta = entrada.readLine();
System.out.println("Recibido: " + respuesta);java
PrintWriter(socket.getOutputStream(), true) — el segundo parámetro true activa el auto-flush, así no necesitas llamar a flush() manualmente tras cada println().🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Cliente que envía un datagrama con una palabra y servidor que responde la longitud.
FLUJO Pasos mentales
- Crear socket UDP
- Preparar paquete
- Enviar
- Recibir respuesta
🎯 Mini ejercicio tipo examen
Haz un cliente UDP que mande “ping” y un servidor que responda “pong”.
⚡ Resumen ultra rápido
- UDP = sin conexión
- Más simple y rápido
- No garantiza entrega ni orden
📦 UDP — Sin conexión, con datagramas
En UDP no hay conexión persistente. Cada mensaje se envía como un DatagramPacket independiente a través de un DatagramSocket.
// ─── Servidor UDP ───
public class ServidorUDP {
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket(5001)) {
System.out.println("Servidor UDP escuchando en puerto 5001...");
byte[] buffer = new byte[1024];
// Recibir datagrama del cliente
DatagramPacket paqueteEntrada = new DatagramPacket(buffer, buffer.length);
socket.receive(paqueteEntrada); // BLOQUEANTE
String mensaje = new String(paqueteEntrada.getData(),
0, paqueteEntrada.getLength());
System.out.println("Recibido: " + mensaje);
// Responder al cliente (usamos la IP y puerto del paquete recibido)
String respuesta = "ACK: " + mensaje.toUpperCase();
byte[] bufResp = respuesta.getBytes();
DatagramPacket paqueteSalida = new DatagramPacket(
bufResp, bufResp.length,
paqueteEntrada.getAddress(), // IP del cliente
paqueteEntrada.getPort() // Puerto del cliente
);
socket.send(paqueteSalida);
System.out.println("Respuesta enviada.");
} catch (IOException e) { e.printStackTrace(); }
}
}
// ─── Cliente UDP ───
public class ClienteUDP {
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket()) {
InetAddress direccionServidor = InetAddress.getByName("localhost");
int puertoServidor = 5001;
// Enviar mensaje al servidor
String mensaje = "hola desde el cliente";
byte[] buf = mensaje.getBytes();
DatagramPacket paquete = new DatagramPacket(
buf, buf.length, direccionServidor, puertoServidor
);
socket.send(paquete);
System.out.println("Enviado: " + mensaje);
// Recibir respuesta
byte[] bufResp = new byte[1024];
DatagramPacket respuesta = new DatagramPacket(bufResp, bufResp.length);
socket.receive(respuesta);
System.out.println("Respuesta: " + new String(
respuesta.getData(), 0, respuesta.getLength()));
} catch (IOException e) { e.printStackTrace(); }
}
}java
📡 Diferencia TCP vs UDP
| TCP | UDP |
|---|---|
| Orientado a conexión | Sin conexión |
| Garantiza entrega y orden | No garantiza nada |
| Más lento (confirmaciones) | Más rápido |
| ServerSocket + Socket | DatagramSocket + DatagramPacket |
| Chat, transferencia archivos | Streaming, videojuegos, DNS |
🔵 UDP — Servidor y Cliente completo
// ─── ServidorUDP.java ───
public class ServidorUDP {
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket(5000)) {
System.out.println("Servidor UDP escuchando en puerto 5000...");
byte[] buffer = new byte[1024];
while (true) {
// Esperar datagrama (paquete) del cliente
DatagramPacket paquete = new DatagramPacket(buffer, buffer.length);
socket.receive(paquete); // bloquea hasta recibir
// Leer el mensaje
String mensaje = new String(paquete.getData(), 0, paquete.getLength());
System.out.println("Cliente dice: " + mensaje);
// Enviar respuesta al mismo cliente
String respuesta = "Recibido: " + mensaje;
byte[] datosResp = respuesta.getBytes();
DatagramPacket resp = new DatagramPacket(
datosResp,
datosResp.length,
paquete.getAddress(), // IP del cliente
paquete.getPort() // Puerto del cliente
);
socket.send(resp);
}
} catch (IOException e) { e.printStackTrace(); }
}
}
// ─── ClienteUDP.java ───
public class ClienteUDP {
public static void main(String[] args) {
try (DatagramSocket socket = new DatagramSocket();
Scanner sc = new Scanner(System.in)) {
InetAddress servidor = InetAddress.getByName("localhost");
int puerto = 5000;
byte[] buffer = new byte[1024];
System.out.print("Mensaje: ");
String msg = sc.nextLine();
byte[] datos = msg.getBytes();
// Enviar datagrama al servidor
DatagramPacket paquete = new DatagramPacket(datos, datos.length, servidor, puerto);
socket.send(paquete);
// Esperar respuesta
DatagramPacket resp = new DatagramPacket(buffer, buffer.length);
socket.receive(resp);
System.out.println("Servidor: " + new String(resp.getData(), 0, resp.getLength()));
} catch (IOException e) { e.printStackTrace(); }
}
}java
DatagramSocket en ambos lados. El "servidor" simplemente escucha en un puerto. No hay conexión previa.🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Servidor que acepta clientes en bucle y crea un hilo por conexión para atender mensajes.
FLUJO Pasos mentales
- Aceptar cliente
- Crear hilo manejador
- Procesar comunicación
- Cerrar recursos
🎯 Mini ejercicio tipo examen
Diseña un servidor que acepte muchos clientes y devuelva la hora actual.
⚡ Resumen ultra rápido
- Bucle de aceptación + hilo por cliente
- Cada hilo atiende una conexión
- Cierra sockets siempre
🏗️ Arquitectura de un servidor multihilo
Un servidor que atiende a varios clientes a la vez. Por cada cliente que conecta, el servidor crea un hilo dedicado para atenderlo.
// Estructura mínima de 4 clases (recomendada):
//
// Servidor.java → ServerSocket, acepta conexiones, lanza hilos del servidor
// ServidorHilo.java → Hilo del servidor. Gestiona UN cliente. Tiene la lógica del servidor.
// Cliente.java → Lanza hilos de cliente
// ClienteHilo.java → Hilo del cliente. Tiene el menú y las opciones del usuario.estructura
📡 Servidor.java — Acepta conexiones en bucle infinito
public class Servidor {
private static final int PUERTO = 5000;
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket()) {
serverSocket.bind(new InetSocketAddress("localhost", PUERTO));
System.out.println("Servidor activo en puerto " + PUERTO);
// Bucle infinito: el servidor siempre está escuchando
while (true) {
Socket socket = serverSocket.accept(); // espera cliente
System.out.println("Cliente conectado: " + socket.getInetAddress());
// Crea un hilo dedicado para este cliente
Thread hiloServidor = new Thread(new ServidorHilo(socket));
hiloServidor.start();
}
} catch (IOException e) { e.printStackTrace(); }
}
}java
⚙️ ServidorHilo.java — Atiende a un cliente concreto
// Ejemplo: gestión de un fichero de notas de alumnos
public class ServidorHilo implements Runnable {
private Socket socket;
private BufferedReader entrada;
private PrintWriter salida;
// Recurso compartido entre TODOS los hilos del servidor
private static final String FICHERO = "notas.txt";
private static final Object LOCK = new Object(); // para synchronized
public ServidorHilo(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
entrada = new BufferedReader(new InputStreamReader(socket.getInputStream()));
salida = new PrintWriter(socket.getOutputStream(), true);
String opcion;
while (!(opcion = entrada.readLine()).equals("5")) { // 5=salir
String nombre = entrada.readLine();
switch (opcion) {
case "1": insertar(nombre, entrada.readLine()); break; // nota
case "2": modificar(nombre, entrada.readLine()); break;
case "3": consultar(nombre); break;
case "4": eliminar(nombre); break;
}
}
salida.println("Hasta luego");
} catch (IOException e) {
e.printStackTrace();
} finally {
try { socket.close(); } catch (IOException e) {}
}
}
// synchronized en escritura: solo 1 hilo puede escribir a la vez
private void insertar(String nombre, String nota) {
synchronized (LOCK) { // bloquea el acceso al fichero para escritura
try (PrintWriter pw = new PrintWriter(new FileWriter(FICHERO, true))) {
// Comprobar si el alumno ya existe
if (buscar(nombre) != null) {
salida.println("ERROR: " + nombre + " ya tiene nota");
return;
}
pw.println(nombre + "-" + nota);
salida.println("OK: nota insertada");
} catch (IOException e) { salida.println("ERROR al insertar"); }
}
}
// La lectura NO necesita synchronized (varios pueden leer a la vez)
private String buscar(String nombre) {
try (BufferedReader br = new BufferedReader(new FileReader(FICHERO))) {
String linea;
while ((linea = br.readLine()) != null) {
if (linea.startsWith(nombre + "-")) return linea;
}
} catch (IOException e) {}
return null; // no encontrado
}
private void consultar(String nombre) {
String linea = buscar(nombre);
if (linea != null) salida.println("Nota: " + linea.split("-")[1]);
else salida.println("ERROR: alumno no encontrado");
}
private void eliminar(String nombre) { /* similar a insertar con synchronized */ }
private void modificar(String nombre, String nota) { /* similar */ }
}java
💻 Cliente.java y ClienteHilo.java
// ─── Cliente.java — lanza los hilos de cliente ───
public class Cliente {
public static void main(String[] args) {
// Opción 1: generar varios clientes (para simular concurrencia)
int numClientes = 3;
for (int i = 1; i <= numClientes; i++) {
Thread hiloCliente = new Thread(new ClienteHilo("localhost", 5000, i));
hiloCliente.start();
}
}
}
// ─── ClienteHilo.java — tiene el menú del usuario ───
public class ClienteHilo implements Runnable {
private String host;
private int puerto, idCliente;
public ClienteHilo(String host, int puerto, int id) {
this.host = host;
this.puerto = puerto;
this.idCliente = id;
}
@Override
public void run() {
try (Socket socket = new Socket(host, puerto)) {
PrintWriter salida = new PrintWriter(socket.getOutputStream(), true);
BufferedReader entrada = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Scanner sc = new Scanner(System.in);
String opcion;
do {
System.out.println("[Cliente " + idCliente + "] 1-Insertar 2-Modificar 3-Consultar 4-Eliminar 5-Salir");
opcion = sc.nextLine();
salida.println(opcion); // envía opción al servidor
if (!opcion.equals("5")) {
System.out.print("Nombre: ");
salida.println(sc.nextLine()); // envía nombre
if (opcion.equals("1") || opcion.equals("2")) {
System.out.print("Nota: ");
salida.println(sc.nextLine()); // envía nota
}
String respuesta = entrada.readLine(); // espera respuesta
System.out.println("Servidor: " + respuesta);
}
} while (!opcion.equals("5"));
System.out.println("[Cliente " + idCliente + "] " + entrada.readLine());
} catch (IOException e) { e.printStackTrace(); }
}
}java
📋 Checklist para el examen
- ¿Hay sección crítica? → Si varios hilos acceden al mismo recurso, añade
synchronized(monitor) oSemaphore. La lectura generalmente no necesita bloque; la escritura siempre sí. - ¿El fichero existe? → El fichero de datos tiene que estar creado antes de arrancar, o créalo con código en el constructor.
- Arrancar SIEMPRE el servidor antes que el cliente.
- Comentar el código — el profe pone puntos por los comentarios. Explica qué hace cada método y por qué usas el monitor/semáforo.
- Usar
flush()o auto-flush — sin esto los mensajes pueden no enviarse. - Usar
join()— para esperar a que todos los hilos terminen antes de mostrar resultados finales. - Puerto de 4 cifras ≥ 1024 — los puertos por debajo de 1024 son del sistema.
🚨 Errores más comunes
- Llamar a
run()en vez destart()→ no crea ningún hilo. - No hacer
join()→ el main termina antes de que acaben los hilos y el resultado es incorrecto. - No sincronizar las escrituras al fichero → corrupción de datos o resultados incorrectos.
- Arrancar el cliente antes que el servidor →
ConnectException: Connection refused. - No incluir el fichero de datos →
FileNotFoundExceptional arrancar. - Puerto ya en uso → parar todos los procesos Java en ejecución y volver a arrancar.
- Usar
ifen vez dewhileconwait()→ vulnerabilidad a spurious wakeups.
🧪 En práctica — cómo pensarlo
CASO REAL Ejemplo de uso
Comparativa de planificación, ejercicio de hilos o cliente-servidor sencillo.
FLUJO Pasos mentales
- Detectar si es teoría o código
- Escribir patrón mínimo
- Añadir explicación de sincronización o comunicación
🎯 Mini ejercicio tipo examen
Resume cuándo usarías Thread, semáforo y socket TCP.
⚡ Resumen ultra rápido
- Usa ejemplos reales
- Dibuja estados o flujos si ayuda
- No confundas proceso con hilo
📋 Patrón del examen
| Pregunta | Tema | Patrón |
|---|---|---|
| 1 | Semáforos | Recurso compartido con N permisos (pabellón, parque, parking) |
| 2 | Monitor + Socket | Servidor con fichero/cuenta bancaria + synchronized + cliente con menú |
| 3 | Socket + fichero | Servidor guarda número en fichero, cliente adivina/consulta (lotería, juego) |
⚽ EXAMEN 24-25 — Semáforos: Pabellón con 10 pistas y 8 balones
import java.util.concurrent.Semaphore;
import java.util.ArrayList;
// El enunciado: 10 pistas, 8 balones. Equipos piden balón (acquire),
// devuelven al terminar (release). Si hay alguien esperando, se lo dan directamente.
// "dar directamente" = el semáforo ya lo gestiona: release() despierta al siguiente acquire().
public class Equipo implements Runnable {
private int idEquipo;
// 8 balones = 8 permisos en el semáforo
private static final Semaphore balones = new Semaphore(8);
public Equipo(int id) { this.idEquipo = id; }
@Override
public void run() {
try {
System.out.printf("Equipo %d quiere jugar, pide balón...%n", idEquipo);
// Pedir balón — si no hay, espera bloqueado
balones.acquire();
System.out.printf("Equipo %d tiene balón, jugando partido (balones disp: %d)%n",
idEquipo, balones.availablePermits());
// Simula duración del partido
Thread.sleep(2000 + (int)(Math.random() * 2000));
System.out.printf("Equipo %d termina partido, devuelve balón%n", idEquipo);
// Devolver balón — si hay equipos esperando, el siguiente acquire() continúa
balones.release();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
// 10 pistas = 10 equipos quieren jugar
ArrayList<Thread> hilos = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
Thread t = new Thread(new Equipo(i));
hilos.add(t);
t.start();
}
for (Thread t : hilos) t.join();
System.out.println("Todos los partidos terminados.");
}
}java
🎡 EXAMEN anterior — Semáforos: Parque con 3 puertas (contadores)
// 3 puertas (3 hilos). Contar visitantes por puerta y total (variable estática).
public class Puerta implements Runnable {
private int numPuerta;
private int visitantesPuerta;
// Variable estática: compartida entre TODAS las puertas
private static int totalVisitantes = 0;
public Puerta(int numPuerta) {
this.numPuerta = numPuerta;
this.visitantesPuerta = 0;
}
@Override
public void run() {
// Simula visitantes entrando por esta puerta
java.util.Random rnd = new java.util.Random();
int numVisitantes = rnd.nextInt(20) + 10; // entre 10 y 30 visitantes
for (int i = 0; i < numVisitantes; i++) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
visitantesPuerta++;
totalVisitantes++; // afecta a todas las puertas
System.out.printf("Puerta %d: entra visitante %d%n", numPuerta, visitantesPuerta);
}
System.out.printf("=== Puerta %d cerrada. Visitantes por esta puerta: %d ===%n",
numPuerta, visitantesPuerta);
}
public static void main(String[] args) throws InterruptedException {
Thread p1 = new Thread(new Puerta(1));
Thread p2 = new Thread(new Puerta(2));
Thread p3 = new Thread(new Puerta(3));
p1.start(); p2.start(); p3.start();
p1.join(); p2.join(); p3.join();
System.out.println("TOTAL visitantes al parque: " + totalVisitantes);
}
}java
🏦 EXAMEN 24-25 — Monitor + Socket: Cuenta bancaria compartida
// ─── CuentaBancaria.java (recurso compartido) ───
public class CuentaBancaria {
private static double saldo = 1000.0;
public synchronized void ingresar(double cantidad) throws InterruptedException {
Thread.sleep(200);
saldo += cantidad;
System.out.printf("INGRESO %.2f€ → saldo: %.2f€%n", cantidad, saldo);
}
public synchronized void retirar(double cantidad) throws InterruptedException {
if (cantidad > saldo) { System.out.println("Saldo insuficiente"); return; }
Thread.sleep(200);
saldo -= cantidad;
System.out.printf("RETIRADA %.2f€ → saldo: %.2f€%n", cantidad, saldo);
}
public synchronized double getSaldo() { return saldo; }
}
// ─── Servidor.java ───
public class Servidor {
public static void main(String[] args) {
CuentaBancaria cuenta = new CuentaBancaria();
try (ServerSocket ss = new ServerSocket()) {
ss.bind(new InetSocketAddress("localhost", 5000));
System.out.println("Servidor bancario activo...");
while (true) {
Socket s = ss.accept();
new Thread(new ServidorHilo(s, cuenta)).start();
}
} catch (IOException e) { e.printStackTrace(); }
}
}
// ─── ServidorHilo.java ───
public class ServidorHilo implements Runnable {
private Socket socket;
private CuentaBancaria cuenta;
public ServidorHilo(Socket s, CuentaBancaria c) { socket=s; cuenta=c; }
@Override
public void run() {
try (
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter pw = new PrintWriter(socket.getOutputStream(), true)
) {
String op;
while (!(op = br.readLine()).equals("3")) {
switch (op) {
case "1": // ingresar
double ing = Double.parseDouble(br.readLine());
cuenta.ingresar(ing);
pw.println("OK ingreso " + ing + "€. Saldo: " + cuenta.getSaldo());
break;
case "2": // retirar
double ret = Double.parseDouble(br.readLine());
cuenta.retirar(ret);
pw.println("OK retirada " + ret + "€. Saldo: " + cuenta.getSaldo());
break;
}
pw.println("Saldo actual: " + cuenta.getSaldo() + "€");
}
pw.println("Hasta luego");
} catch (Exception e) { e.printStackTrace(); }
}
}
// ─── Cliente.java ───
public class Cliente {
public static void main(String[] args) {
try (Socket s = new Socket("localhost", 5000);
PrintWriter pw = new PrintWriter(s.getOutputStream(), true);
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
Scanner sc = new Scanner(System.in)
) {
String op;
do {
System.out.println("1-Ingresar 2-Retirar 3-Salir");
op = sc.nextLine();
pw.println(op);
if (op.equals("1") || op.equals("2")) {
System.out.print("Cantidad: ");
pw.println(sc.nextLine());
System.out.println("Servidor: " + br.readLine());
}
} while (!op.equals("3"));
System.out.println(br.readLine());
} catch (IOException e) { e.printStackTrace(); }
}
}java
🎲 EXAMEN 24-25 — Socket + fichero: Adivinar número (juego.txt)
// ─── Servidor.java ───
// Al arrancar: guarda número aleatorio 0-20 en juego.txt
// Recibe intentos del cliente, responde mayor/menor/acertado
// Se cierra cuando el cliente acierta
public class Servidor {
public static void main(String[] args) {
int secreto = (int)(Math.random() * 21); // 0-20
// Guardar en juego.txt
try (PrintWriter pw = new PrintWriter(new FileWriter("juego.txt"))) {
pw.println(secreto);
} catch (IOException e) { e.printStackTrace(); }
System.out.println("Número guardado en juego.txt");
try (ServerSocket ss = new ServerSocket()) {
ss.bind(new InetSocketAddress("localhost", 5000));
System.out.println("Servidor escuchando...");
// Seguir escuchando hasta que el cliente acierte
boolean acertado = false;
while (!acertado) {
Socket s = ss.accept();
try (
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
PrintWriter pw = new PrintWriter(s.getOutputStream(), true)
) {
String linea;
while ((linea = br.readLine()) != null) {
int intento = Integer.parseInt(linea);
if (intento == secreto) {
pw.println("¡ACERTADO! El número era " + secreto);
acertado = true;
break;
} else if (intento < secreto) {
pw.println("MAYOR — el número es mayor que " + intento);
} else {
pw.println("MENOR — el número es menor que " + intento);
}
}
}
}
System.out.println("Número acertado. Servidor cerrado.");
} catch (IOException e) { e.printStackTrace(); }
}
}
// ─── Cliente.java ───
public class Cliente {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
boolean acertado = false;
while (!acertado) {
try (
Socket s = new Socket("localhost", 5000);
PrintWriter pw = new PrintWriter(s.getOutputStream(), true);
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()))
) {
System.out.print("Introduce un número (0-20): ");
String intento = sc.nextLine();
pw.println(intento);
String respuesta = br.readLine();
System.out.println("Servidor: " + respuesta);
if (respuesta.startsWith("¡ACERTADO")) acertado = true;
} catch (IOException e) { e.printStackTrace(); }
}
}
}
java
🎯 EXAMEN anterior — Monitor wait/notify: Tinaja (llenado + vaciado)
// Tinaja: llena a 10L/s → a 900L activa vaciado 5L/s
// → a 1000L para llenado, vacía a 10L/s
// → a 100L activa llenado 5L/s
// → a 0L para vaciado, llena a 10L/s (ciclo)
public class Tinaja {
private double litros = 0;
private boolean llenadoActivo = true;
private boolean vaCiadoActivo = false;
private double ritmoLlenado = 10, ritmoVaciado = 0;
public synchronized void llenar() throws InterruptedException {
while (!llenadoActivo) wait();
litros += ritmoLlenado;
System.out.printf("[LLENADO] %.0f L (ritmo: %.0f L/s)%n", litros, ritmoLlenado);
if (litros >= 900 && !vaCiadoActivo) {
vaCiadoActivo = true; ritmoVaciado = 5;
System.out.println(">> 900L: activando vaciado a 5L/s");
notifyAll();
}
if (litros >= 1000) {
llenadoActivo = false; ritmoLlenado = 0; ritmoVaciado = 10;
System.out.println(">> 1000L: parando llenado, vaciado a 10L/s");
notifyAll();
}
Thread.sleep(300);
}
public synchronized void vaciar() throws InterruptedException {
while (!vaCiadoActivo) wait();
litros -= ritmoVaciado;
if (litros < 0) litros = 0;
System.out.printf("[VACIADO] %.0f L (ritmo: %.0f L/s)%n", litros, ritmoVaciado);
if (litros <= 100 && !llenadoActivo) {
llenadoActivo = true; ritmoLlenado = 5;
System.out.println(">> 100L: activando llenado a 5L/s");
notifyAll();
}
if (litros <= 0) {
vaCiadoActivo = false; ritmoVaciado = 0; ritmoLlenado = 10;
System.out.println(">> 0L: parando vaciado, llenado a 10L/s");
notifyAll();
}
Thread.sleep(300);
}
}
// ─── HiloLlenado y HiloVaciado ───
class HiloLlenado implements Runnable {
private Tinaja t; private int ciclos;
public HiloLlenado(Tinaja t, int c) { this.t=t; ciclos=c; }
@Override public void run() {
for (int i=0; i<ciclos; i++) try { t.llenar(); } catch (InterruptedException e) {}
}
}
class HiloVaciado implements Runnable {
private Tinaja t; private int ciclos;
public HiloVaciado(Tinaja t, int c) { this.t=t; ciclos=c; }
@Override public void run() {
for (int i=0; i<ciclos; i++) try { t.vaciar(); } catch (InterruptedException e) {}
}
}
// ─── Main ───
public class Main {
public static void main(String[] args) throws InterruptedException {
Tinaja tinaja = new Tinaja();
Thread tLlen = new Thread(new HiloLlenado(tinaja, 200));
Thread tVac = new Thread(new HiloVaciado(tinaja, 200));
tLlen.start(); tVac.start();
tLlen.join(); tVac.join();
}
}java
🎰 EXAMEN anterior — Socket + fichero: Lotería
// Servidor: tiene número premiado en fichero. Cliente envía su número,
// servidor responde si ganó o no.
public class ServidorLoteria {
public static void main(String[] args) {
// Leer número del fichero
int premiado = 0;
try (BufferedReader br = new BufferedReader(new FileReader("loteria.txt"))) {
premiado = Integer.parseInt(br.readLine().trim());
} catch (IOException e) { e.printStackTrace(); }
System.out.println("Número premiado cargado. Esperando clientes...");
final int numPremiado = premiado;
try (ServerSocket ss = new ServerSocket()) {
ss.bind(new InetSocketAddress("localhost", 5000));
while (true) {
Socket s = ss.accept();
new Thread(() -> {
try (
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
PrintWriter pw = new PrintWriter(s.getOutputStream(), true)
) {
String linea = br.readLine();
int numCliente = Integer.parseInt(linea);
if (numCliente == numPremiado)
pw.println("¡PREMIADO! Has ganado con el número " + numCliente);
else
pw.println("Lo sentimos, el número " + numCliente + " no ha sido premiado");
} catch (IOException e) {}
}).start();
}
} catch (IOException e) { e.printStackTrace(); }
}
}java